From eea6fc84f20185c4a15047d945c5a37c1043a91b Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 15 Sep 2026 17:08:59 +0200 Subject: [PATCH 1/4] fix(doctrine): raise orm floor to ^3.3 Doctrine\ORM\Query\AST\PartialObjectExpression is absent from doctrine/orm 3.0.0 through 3.2.3 and returns in 3.3.0. The class_exists() guard in EagerLoadingExtension silently turned fetchPartial off on those versions, making the advertised fetch_partial option a no-op and failing the component's --prefer-lowest CI job. Raise the floor to ^2.17 || ^3.3 and drop the now-dead guard. Also require doctrine/doctrine-bundle ^2.11.1: 2.11.0 passes $reportFieldsWhereDeclared to AttributeDriver, which doctrine/orm 3 rejects. --- src/Doctrine/Orm/Extension/EagerLoadingExtension.php | 3 +-- src/Doctrine/Orm/composer.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index 01a54bcedc1..d68afc91b7d 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -23,7 +23,6 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Query\AST\PartialObjectExpression; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Select; use Doctrine\ORM\QueryBuilder; @@ -71,7 +70,7 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $ $options = []; $forceEager = $operation?->getForceEager() ?? $this->forceEager; - $fetchPartial = class_exists(PartialObjectExpression::class) && ($operation?->getFetchPartial() ?? $this->fetchPartial); + $fetchPartial = $operation?->getFetchPartial() ?? $this->fetchPartial; if (!isset($context['groups']) && !isset($context['attributes'])) { $contextType = isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context'; diff --git a/src/Doctrine/Orm/composer.json b/src/Doctrine/Orm/composer.json index 121eba93fa5..6f84a8df861 100644 --- a/src/Doctrine/Orm/composer.json +++ b/src/Doctrine/Orm/composer.json @@ -29,10 +29,10 @@ "api-platform/serializer": "^4.4@beta", "api-platform/state": "^4.4@beta", "composer/semver": "^3.4", - "doctrine/orm": "^2.17 || ^3.0.1" + "doctrine/orm": "^2.17 || ^3.3" }, "require-dev": { - "doctrine/doctrine-bundle": "^2.11 || ^3.1", + "doctrine/doctrine-bundle": "^2.11.1 || ^3.1", "phpspec/prophecy-phpunit": "^2.2", "phpunit/phpunit": "^11.5 || ^12.2", "ramsey/uuid": "^4.7", From 64c8daa7c67fb806feef14694076595584044998 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 15 Sep 2026 17:09:06 +0200 Subject: [PATCH 2/4] test(doctrine): use phpunit mocks in eager loading Prophecy returned null for unstubbed calls, so a wrong DQL string surfaced as a TypeError rather than an assertion failure. Assert the emitted selects explicitly. createMock() returns a typed ClassMetadata, which exposes the ORM 2/3 disagreement on the shape of $associationMappings; the fixtures stay arrays and the mismatch is ignored for this file only. --- phpstan.neon.dist | 7 + .../Extension/EagerLoadingExtensionTest.php | 1271 +++++++++-------- 2 files changed, 719 insertions(+), 559 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2015d5831b1..a5b4a1e3712 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -78,6 +78,13 @@ parameters: - tests/Fixtures/TestBundle/Document/ - src/Metadata/Tests/Fixtures/ApiResource/ - '#Access to an undefined property Prophecy\\Prophecy\\ObjectProphecy<(\\?[a-zA-Z0-9_]+)+>::\$[a-zA-Z0-9_]+#' + # ORM 2 and 3 disagree on the shape of ClassMetadata::$associationMappings + # (raw arrays vs mapping objects). The extension reads them through + # ArrayAccess, so array fixtures are valid for both; composer still + # supports doctrine/orm ^2.17. + - + identifier: assign.propertyType + path: src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php # https://github.com/phpstan/phpstan-symfony/issues/76 - message: '#Service "test" is not registered in the container\.#' diff --git a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php index 1eb400bc20d..87a2fe4f211 100644 --- a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php +++ b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php @@ -33,15 +33,13 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; -use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; @@ -53,27 +51,24 @@ */ class EagerLoadingExtensionTest extends TestCase { - use ProphecyTrait; - public function testApplyToCollection(): void { $context = ['groups' => ['foo']]; $callContext = ['serializer_groups' => ['foo']]; - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['id', 'name', 'notindatabase', 'notreadable', 'embeddedDummy']); $relatedEmbedableCollection = new PropertyNameCollection(['name']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); - - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy2', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyNameCollectionFactoryProphecy->create(EmbeddableDummy::class)->willReturn($relatedEmbedableCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [RelatedDummy::class, $relatedNameCollection], + [EmbeddableDummy::class, $relatedEmbedableCollection], + ]); $idPropertyMetadata = new ApiProperty(); $idPropertyMetadata = $idPropertyMetadata->withIdentifier(true); @@ -86,50 +81,70 @@ public function testApplyToCollection(): void $notReadablePropertyMetadata = new ApiProperty(); $notReadablePropertyMetadata = $notReadablePropertyMetadata->withReadable(false); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', $callContext)->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', $callContext)->willReturn($namePropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'embeddedDummy', $callContext)->willReturn($embeddedPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'notindatabase', $callContext)->willReturn($notInDatabasePropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'notreadable', $callContext)->willReturn($notReadablePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', $callContext, $relationPropertyMetadata], + [Dummy::class, 'relatedDummy2', $callContext, $relationPropertyMetadata], + [RelatedDummy::class, 'id', $callContext, $idPropertyMetadata], + [RelatedDummy::class, 'name', $callContext, $namePropertyMetadata], + [RelatedDummy::class, 'embeddedDummy', $callContext, $embeddedPropertyMetadata], + [RelatedDummy::class, 'notindatabase', $callContext, $notInDatabasePropertyMetadata], + [RelatedDummy::class, 'notreadable', $callContext, $notReadablePropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $hasFieldMap = []; foreach ($relatedNameCollection as $property) { if ('id' !== $property && 'embeddedDummy' !== $property) { - $relatedClassMetadataProphecy->hasField($property)->willReturn('notindatabase' !== $property)->shouldBeCalled(); + $hasFieldMap[] = [$property, 'notindatabase' !== $property]; } } - $relatedClassMetadataProphecy->hasField('embeddedDummy.name')->willReturn(true)->shouldBeCalled(); - - $relatedClassMetadataProphecy->embeddedClasses = ['embeddedDummy' => ['class' => EmbeddableDummy::class]]; + $hasFieldMap[] = ['embeddedDummy.name', true]; + $relatedClassMetadataMock->expects($this->atLeastOnce())->method('hasField')->willReturnMap($hasFieldMap); - $relatedClassMetadataProphecy->associationMappings = []; + $relatedClassMetadataMock->embeddedClasses = ['embeddedDummy' => ['class' => EmbeddableDummy::class]]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $relatedClassMetadataMock->associationMappings = []; - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy2_a2.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $queryBuilderMock->expects($this->exactly(1))->method('leftJoin')->with('o.relatedDummy', 'relatedDummy_a1')->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->exactly(1))->method('innerJoin')->with('o.relatedDummy2', 'relatedDummy2_a2')->willReturn($queryBuilderMock); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(2))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, null, $context); + + $this->assertSame([ + 'partial relatedDummy_a1.{id,name,embeddedDummy.name}', + 'partial relatedDummy2_a2.{id,name,embeddedDummy.name}', + ], $actualSelects); } public function testApplyToItem(): void @@ -137,28 +152,22 @@ public function testApplyToItem(): void $context = ['groups' => ['foo']]; $callContext = ['serializer_groups' => ['foo']]; - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['id', 'name', 'embeddedDummy', 'notindatabase', 'notreadable', 'relation']); $relatedEmbedableCollection = new PropertyNameCollection(['name']); - $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(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [RelatedDummy::class, $relatedNameCollection], + [EmbeddableDummy::class, $relatedEmbedableCollection], + [UnknownDummy::class, new PropertyNameCollection(['id'])], + [ThirdLevel::class, new PropertyNameCollection(['id'])], + ]); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy2', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy3', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $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); $namePropertyMetadata = new ApiProperty(); @@ -170,20 +179,29 @@ public function testApplyToItem(): void $notReadablePropertyMetadata = new ApiProperty(); $notReadablePropertyMetadata = $notReadablePropertyMetadata->withReadable(false); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', $callContext)->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', $callContext)->willReturn($namePropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'embeddedDummy', $callContext)->willReturn($embeddedDummyPropertyMetadata)->shouldBeCalled(); - $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(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', $callContext, $relationPropertyMetadata], + [Dummy::class, 'relatedDummy2', $callContext, $relationPropertyMetadata], + [Dummy::class, 'relatedDummy3', $callContext, $relationPropertyMetadata], + [Dummy::class, 'relatedDummy4', $callContext, $relationPropertyMetadata], + [Dummy::class, 'relatedDummy5', $callContext, $relationPropertyMetadata], + [Dummy::class, 'singleInheritanceRelation', $callContext, $relationPropertyMetadata], + [Dummy::class, 'relatedDummies', $callContext, $relationPropertyMetadata], + [RelatedDummy::class, 'id', $callContext, $idPropertyMetadata], + [RelatedDummy::class, 'name', $callContext, $namePropertyMetadata], + [RelatedDummy::class, 'embeddedDummy', $callContext, $embeddedDummyPropertyMetadata], + [RelatedDummy::class, 'notindatabase', $callContext, $notInDatabasePropertyMetadata], + [RelatedDummy::class, 'notreadable', $callContext, $notReadablePropertyMetadata], + [RelatedDummy::class, 'relation', $callContext, $relationPropertyMetadata], + [RelatedDummy::class, 'thirdLevel', $callContext, $relationPropertyMetadata], + [UnknownDummy::class, 'id', $callContext, $idPropertyMetadata], + [ThirdLevel::class, 'id', $callContext, $idPropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => UnknownDummy::class], 'relatedDummy3' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinTable' => ['joinColumns' => [new JoinColumn(nullable: false)]], 'targetEntity' => UnknownDummy::class], @@ -193,280 +211,322 @@ public function testApplyToItem(): void 'relatedDummies' => ['fetch' => ClassMetadata::FETCH_EAGER, 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $hasFieldMap = []; foreach ($relatedNameCollection as $property) { if ('id' !== $property && 'embeddedDummy' !== $property) { - $relatedClassMetadataProphecy->hasField($property)->willReturn('notindatabase' !== $property)->shouldBeCalled(); + $hasFieldMap[] = [$property, 'notindatabase' !== $property]; } } - $relatedClassMetadataProphecy->hasField('embeddedDummy.name')->willReturn(true)->shouldBeCalled(); + $hasFieldMap[] = ['embeddedDummy.name', true]; + $relatedClassMetadataMock->expects($this->atLeastOnce())->method('hasField')->willReturnMap($hasFieldMap); - $relatedClassMetadataProphecy->associationMappings = [ + $relatedClassMetadataMock->associationMappings = [ 'relation' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => UnknownDummy::class], 'thirdLevel' => ['fetch' => ClassMetadata::FETCH_EAGER, 'targetEntity' => ThirdLevel::class, 'sourceEntity' => RelatedDummy::class, 'inversedBy' => 'relatedDummies', 'type' => ClassMetadata::TO_ONE], ]; - $relatedClassMetadataProphecy->embeddedClasses = ['embeddedDummy' => ['class' => EmbeddableDummy::class]]; - - $singleInheritanceClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $singleInheritanceClassMetadataProphecy->subClasses = [ConcreteDummy::class]; - - $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->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('relatedDummy_a1.relation', 'relation_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('relatedDummy_a1.thirdLevel', 'thirdLevel_a3')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a4')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummy3', 'relatedDummy3_a5')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummy4', 'relatedDummy4_a6')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.singleInheritanceRelation', 'singleInheritanceRelation_a7')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummies', 'relatedDummies_a8')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('relatedDummies_a8.relation', 'relation_a9')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('relatedDummies_a8.thirdLevel', 'thirdLevel_a10')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial thirdLevel_a3.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relation_a2.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy2_a4.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy3_a5.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy4_a6.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('singleInheritanceRelation_a7')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummies_a8.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relation_a9.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial thirdLevel_a10.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - - $queryBuilder = $queryBuilderProphecy->reveal(); - $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); + $relatedClassMetadataMock->embeddedClasses = ['embeddedDummy' => ['class' => EmbeddableDummy::class]]; + + $singleInheritanceClassMetadataMock = $this->createMock(ClassMetadata::class); + $singleInheritanceClassMetadataMock->subClasses = [ConcreteDummy::class]; + + $unknownClassMetadataMock = $this->createMock(ClassMetadata::class); + $unknownClassMetadataMock->associationMappings = []; + + $thirdLevelMetadataMock = $this->createMock(ClassMetadata::class); + $thirdLevelMetadataMock->associationMappings = []; + + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + [AbstractDummy::class, $singleInheritanceClassMetadataMock], + [UnknownDummy::class, $unknownClassMetadataMock], + [ThirdLevel::class, $thirdLevelMetadataMock], + ]); + + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + $queryBuilderMock->expects($this->exactly(1))->method('innerJoin')->with('o.relatedDummy2', 'relatedDummy2_a4')->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->exactly(9))->method('leftJoin')->willReturnMap([ + ['o.relatedDummy', 'relatedDummy_a1', $queryBuilderMock], + ['relatedDummy_a1.relation', 'relation_a2', $queryBuilderMock], + ['relatedDummy_a1.thirdLevel', 'thirdLevel_a3', $queryBuilderMock], + ['o.relatedDummy3', 'relatedDummy3_a5', $queryBuilderMock], + ['o.relatedDummy4', 'relatedDummy4_a6', $queryBuilderMock], + ['o.singleInheritanceRelation', 'singleInheritanceRelation_a7', $queryBuilderMock], + ['o.relatedDummies', 'relatedDummies_a8', $queryBuilderMock], + ['relatedDummies_a8.relation', 'relation_a9', $queryBuilderMock], + ['relatedDummies_a8.thirdLevel', 'thirdLevel_a10', $queryBuilderMock], + ]); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(10))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ['select', []], + ]); + + $queryBuilder = $queryBuilderMock; + $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); $orderExtensionTest->applyToItem($queryBuilder, new QueryNameGenerator(), Dummy::class, [], null, $context); + + // addSelect calls are grouped by target entity (not emitted in traversal order), so the exact + // interleaving between independently-joined branches is an aggregation implementation detail, + // not something this test asserts on; compare as sets instead of an ordered sequence. + $this->assertEqualsCanonicalizing([ + 'partial relatedDummy_a1.{id,name,embeddedDummy.name}', + 'partial thirdLevel_a3.{id}', + 'partial relation_a2.{id}', + 'partial relatedDummy2_a4.{id}', + 'partial relatedDummy3_a5.{id}', + 'partial relatedDummy4_a6.{id}', + 'singleInheritanceRelation_a7', + 'partial relatedDummies_a8.{id,name,embeddedDummy.name}', + 'partial relation_a9.{id}', + 'partial thirdLevel_a10.{id}', + ], $actualSelects); } public function testCreateItemWithOperation(): void { - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', ['serializer_groups' => ['foo']])->shouldBeCalled()->willReturn(new ApiProperty()); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->with(Dummy::class, 'foo', ['serializer_groups' => ['foo']])->willReturn(new ApiProperty()); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'foo' => ['fetch' => 1], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); - $eagerExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, [], new Get(name: 'item_operation'), ['groups' => ['foo']]); + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); + $eagerExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), Dummy::class, [], new Get(name: 'item_operation'), ['groups' => ['foo']]); } public function testCreateCollectionWithOperation(): void { - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', ['serializer_groups' => ['foo']])->shouldBeCalled()->willReturn(new ApiProperty()); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->with(Dummy::class, 'foo', ['serializer_groups' => ['foo']])->willReturn(new ApiProperty()); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'foo' => ['fetch' => 1], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); - $eagerExtensionTest->applyToCollection($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, new GetCollection(name: 'collection_operation'), ['groups' => ['foo']]); + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); + $eagerExtensionTest->applyToCollection($queryBuilderMock, new QueryNameGenerator(), Dummy::class, new GetCollection(name: 'collection_operation'), ['groups' => ['foo']]); } public function testDenormalizeItemWithCorrectResourceClass(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = []; + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = []; // Dummy is the correct class for the denormalization context serialization groups, and we're fetching RelatedDummy - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldNotBeCalled(); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); - $eagerExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), RelatedDummy::class, ['id' => 1], new Get(name: 'get', normalizationContext: ['groups' => ['foo']]), ['resource_class' => Dummy::class]); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(RelatedDummy::class)->willReturn($classMetadataMock); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); + $eagerExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), RelatedDummy::class, ['id' => 1], new Get(name: 'get', normalizationContext: ['groups' => ['foo']]), ['resource_class' => Dummy::class]); } public function testDenormalizeItemWithExistingGroups(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = []; + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = []; // groups exist from the context, we don't need to compute them again - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldNotBeCalled(); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); - $eagerExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), RelatedDummy::class, ['id' => 1], new Get(name: 'item_operation', normalizationContext: ['groups' => ['foo']]), [AbstractNormalizer::GROUPS => 'some_groups']); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(RelatedDummy::class)->willReturn($classMetadataMock); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); + $eagerExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), RelatedDummy::class, ['id' => 1], new Get(name: 'item_operation', normalizationContext: ['groups' => ['foo']]), [AbstractNormalizer::GROUPS => 'some_groups']); } public function testContextSwitch(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['id', 'name']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->with(RelatedDummy::class)->willReturn($relatedNameCollection); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $idPropertyMetadata = new ApiProperty(); $idPropertyMetadata = $idPropertyMetadata->withIdentifier(true); $namePropertyMetadata = new ApiProperty(); $namePropertyMetadata = $namePropertyMetadata->withReadable(true); // When called via `relatedDummies` without context switch - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($namePropertyMetadata)->shouldBeCalled(); // When called via `relatedDummy` with context switch - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', ['normalization_groups' => ['bar'], 'denormalization_groups' => ['foo']])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', ['normalization_groups' => ['bar'], 'denormalization_groups' => ['foo']])->willReturn($namePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [RelatedDummy::class, 'id', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $idPropertyMetadata], + [RelatedDummy::class, 'name', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $namePropertyMetadata], + [RelatedDummy::class, 'id', ['normalization_groups' => ['bar'], 'denormalization_groups' => ['foo']], $idPropertyMetadata], + [RelatedDummy::class, 'name', ['normalization_groups' => ['bar'], 'denormalization_groups' => ['foo']], $namePropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummies' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $hasFieldMap = []; foreach ($relatedNameCollection as $property) { if ('id' !== $property && 'embeddedDummy' !== $property) { - $relatedClassMetadataProphecy->hasField($property)->willReturn(true)->shouldBeCalled(); + $hasFieldMap[] = [$property, true]; } } + $relatedClassMetadataMock->expects($this->atLeastOnce())->method('hasField')->willReturnMap($hasFieldMap); - $dummyClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); - $relatedClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); - $classMetadataFactoryProphecy = $this->prophesize(ClassMetadataFactoryInterface::class); + $dummyClassMetadataInterfaceMock = $this->createMock(ClassMetadataInterface::class); + $relatedClassMetadataInterfaceMock = $this->createMock(ClassMetadataInterface::class); + $classMetadataFactoryMock = $this->createMock(ClassMetadataFactoryInterface::class); $relatedDummyAttributeMetadata = new AttributeMetadata('relatedDummy'); $relatedDummyAttributeMetadata->setNormalizationContextForGroups(['groups' => ['bar']], ['foo']); - $dummyClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn(['relatedDummy' => $relatedDummyAttributeMetadata]); - $relatedClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn([]); + $dummyClassMetadataInterfaceMock->method('getAttributesMetadata')->willReturn(['relatedDummy' => $relatedDummyAttributeMetadata]); + $relatedClassMetadataInterfaceMock->method('getAttributesMetadata')->willReturn([]); - $classMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedClassMetadataInterfaceProphecy->reveal()); - $classMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummyClassMetadataInterfaceProphecy->reveal()); + $classMetadataFactoryMock->method('getMetadataFor')->willReturnMap([ + [RelatedDummy::class, $relatedClassMetadataInterfaceMock], + [Dummy::class, $dummyClassMetadataInterfaceMock], + ]); - $relatedClassMetadataProphecy->associationMappings = []; + $relatedClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->leftJoin('o.relatedDummies', 'relatedDummies_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummies_a1.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy_a2.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->expects($this->exactly(2))->method('leftJoin')->willReturnMap([ + ['o.relatedDummies', 'relatedDummies_a1', $queryBuilderMock], + ['o.relatedDummy', 'relatedDummy_a2', $queryBuilderMock], + ]); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(2))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true, $classMetadataFactoryProphecy->reveal()); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true, $classMetadataFactoryMock); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + + $this->assertSame([ + 'partial relatedDummies_a1.{id,name}', + 'partial relatedDummy_a2.{id,name}', + ], $actualSelects); } public function testSameEntityWithDifferentPartialProperties(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['id', 'name']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->with(RelatedDummy::class)->willReturn($relatedNameCollection); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy1', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $idPropertyMetadata = (new ApiProperty())->withIdentifier(true); $namePropertyMetadataGroupA = (new ApiProperty())->withReadable(true); // the property Name IS NOT readable in group B $namePropertyMetadataGroupB = (new ApiProperty())->withReadable(false); // When called via `relatedDummy1` - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', ['normalization_groups' => ['A'], 'denormalization_groups' => ['foo']])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', ['normalization_groups' => ['A'], 'denormalization_groups' => ['foo']])->willReturn($namePropertyMetadataGroupA)->shouldBeCalled(); // When called via `relatedDummy2` - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', ['normalization_groups' => ['B'], 'denormalization_groups' => ['foo']])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', ['normalization_groups' => ['B'], 'denormalization_groups' => ['foo']])->willReturn($namePropertyMetadataGroupB)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy1', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [RelatedDummy::class, 'id', ['normalization_groups' => ['A'], 'denormalization_groups' => ['foo']], $idPropertyMetadata], + [RelatedDummy::class, 'name', ['normalization_groups' => ['A'], 'denormalization_groups' => ['foo']], $namePropertyMetadataGroupA], + [RelatedDummy::class, 'id', ['normalization_groups' => ['B'], 'denormalization_groups' => ['foo']], $idPropertyMetadata], + [RelatedDummy::class, 'name', ['normalization_groups' => ['B'], 'denormalization_groups' => ['foo']], $namePropertyMetadataGroupB], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy1' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $hasFieldMap = []; foreach ($relatedNameCollection as $property) { if ('id' !== $property && 'embeddedDummy' !== $property) { - $relatedClassMetadataProphecy->hasField($property)->willReturn(true)->shouldBeCalled(); + $hasFieldMap[] = [$property, true]; } } + $relatedClassMetadataMock->expects($this->atLeastOnce())->method('hasField')->willReturnMap($hasFieldMap); - $dummyClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); - $relatedClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); - $classMetadataFactoryProphecy = $this->prophesize(ClassMetadataFactoryInterface::class); + $dummyClassMetadataInterfaceMock = $this->createMock(ClassMetadataInterface::class); + $relatedClassMetadataInterfaceMock = $this->createMock(ClassMetadataInterface::class); + $classMetadataFactoryMock = $this->createMock(ClassMetadataFactoryInterface::class); $relatedDummy1AttributeMetadata = new AttributeMetadata('relatedDummy'); $relatedDummy1AttributeMetadata->setNormalizationContextForGroups(['groups' => ['A']], ['foo']); @@ -474,34 +534,50 @@ public function testSameEntityWithDifferentPartialProperties(): void $relatedDummy2AttributeMetadata = new AttributeMetadata('relatedDummy'); $relatedDummy2AttributeMetadata->setNormalizationContextForGroups(['groups' => ['B']], ['foo']); - $dummyClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn([ - 'relatedDummy1' => $relatedDummy1AttributeMetadata, - 'relatedDummy2' => $relatedDummy2AttributeMetadata, - ]); - $relatedClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn([]); + $dummyClassMetadataInterfaceMock->method('getAttributesMetadata')->willReturn(['relatedDummy1' => $relatedDummy1AttributeMetadata, 'relatedDummy2' => $relatedDummy2AttributeMetadata]); + $relatedClassMetadataInterfaceMock->method('getAttributesMetadata')->willReturn([]); - $classMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedClassMetadataInterfaceProphecy->reveal()); - $classMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummyClassMetadataInterfaceProphecy->reveal()); + $classMetadataFactoryMock->method('getMetadataFor')->willReturnMap([ + [RelatedDummy::class, $relatedClassMetadataInterfaceMock], + [Dummy::class, $dummyClassMetadataInterfaceMock], + ]); - $relatedClassMetadataProphecy->associationMappings = []; + $relatedClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->leftJoin('o.relatedDummy1', 'relatedDummy1_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy1_a1.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - // here is the purpose of this test: name is not readable in group B, BUT it is part of the partial selection because it is readable in group A - $queryBuilderProphecy->addSelect('partial relatedDummy2_a2.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->expects($this->exactly(2))->method('leftJoin')->willReturnMap([ + ['o.relatedDummy1', 'relatedDummy1_a1', $queryBuilderMock], + ['o.relatedDummy2', 'relatedDummy2_a2', $queryBuilderMock], + ]); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(2))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true, $classMetadataFactoryProphecy->reveal()); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true, $classMetadataFactoryMock); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + + // here is the purpose of this test: name is not readable in group B, BUT it is part of the partial selection because it is readable in group A + $this->assertSame([ + 'partial relatedDummy1_a1.{id,name}', + 'partial relatedDummy2_a2.{id,name}', + ], $actualSelects); } public function testMaxJoinsReached(): void @@ -509,90 +585,100 @@ public function testMaxJoinsReached(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the "api_platform.eager_loading.max_joins" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the "enable_max_depth" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth).'); - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['dummy']); $dummyNameCollection = new PropertyNameCollection(['relatedDummy']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn($dummyNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [RelatedDummy::class, $relatedNameCollection], + [Dummy::class, $dummyNameCollection], + ]); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo']])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $relatedPropertyMetadata = new ApiProperty(); $relatedPropertyMetadata = $relatedPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'dummy', ['serializer_groups' => ['foo']])->willReturn($relatedPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo']], $relationPropertyMetadata], + [RelatedDummy::class, 'dummy', ['serializer_groups' => ['foo']], $relatedPropertyMetadata], + ]); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; - $classMetadataProphecy->hasField('relatedDummy')->willReturn(true); + $classMetadataMock->method('hasField')->with('relatedDummy')->willReturn(true); - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $relatedClassMetadataProphecy->associationMappings = [ + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $relatedClassMetadataMock->associationMappings = [ 'dummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => Dummy::class], ]; - $relatedClassMetadataProphecy->hasField('dummy')->willReturn(true); + $relatedClassMetadataMock->method('hasField')->with('dummy')->willReturn(true); - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->innerJoin(Argument::type('string'), Argument::type('string'))->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect(Argument::type('string'))->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->method('innerJoin')->with($this->isString(), $this->isString())->willReturn($queryBuilderMock); + $queryBuilderMock->method('addSelect')->with($this->isString())->willReturn($queryBuilderMock); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); - $eagerExtensionTest->applyToCollection($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, null, ['groups' => ['foo']]); + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); + $eagerExtensionTest->applyToCollection($queryBuilderMock, new QueryNameGenerator(), Dummy::class, null, ['groups' => ['foo']]); } public function testMaxDepth(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['dummy']); $dummyNameCollection = new PropertyNameCollection(['relatedDummy']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); - $propertyNameCollectionFactoryProphecy->create(Dummy::class)->willReturn($dummyNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [RelatedDummy::class, $relatedNameCollection], + [Dummy::class, $dummyNameCollection], + ]); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => ['foo']])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $relatedPropertyMetadata = new ApiProperty(); $relatedPropertyMetadata = $relatedPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'dummy', ['serializer_groups' => ['foo'], 'normalization_groups' => ['foo']])->willReturn($relatedPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => ['foo']], $relationPropertyMetadata], + [RelatedDummy::class, 'dummy', ['serializer_groups' => ['foo'], 'normalization_groups' => ['foo']], $relatedPropertyMetadata], + ]); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; - $classMetadataProphecy->hasField('relatedDummy')->willReturn(true); + $classMetadataMock->method('hasField')->with('relatedDummy')->willReturn(true); - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $relatedClassMetadataProphecy->associationMappings = [ + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $relatedClassMetadataMock->associationMappings = [ 'dummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => Dummy::class], ]; - $relatedClassMetadataProphecy->hasField('dummy')->willReturn(true); + $relatedClassMetadataMock->method('hasField')->with('dummy')->willReturn(true); - $dummyClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); - $relatedClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); - $classMetadataFactoryProphecy = $this->prophesize(ClassMetadataFactoryInterface::class); + $dummyClassMetadataInterfaceMock = $this->createMock(ClassMetadataInterface::class); + $relatedClassMetadataInterfaceMock = $this->createMock(ClassMetadataInterface::class); + $classMetadataFactoryMock = $this->createMock(ClassMetadataFactoryInterface::class); $dummyAttributeMetadata = new AttributeMetadata('dummy'); $dummyAttributeMetadata->setMaxDepth(2); @@ -600,366 +686,421 @@ public function testMaxDepth(): void $relatedAttributeMetadata = new AttributeMetadata('relatedDummy'); $relatedAttributeMetadata->setMaxDepth(4); - $dummyClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn(['relatedDummy' => $dummyAttributeMetadata]); - $relatedClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn(['dummy' => $relatedAttributeMetadata]); + $dummyClassMetadataInterfaceMock->method('getAttributesMetadata')->willReturn(['relatedDummy' => $dummyAttributeMetadata]); + $relatedClassMetadataInterfaceMock->method('getAttributesMetadata')->willReturn(['dummy' => $relatedAttributeMetadata]); - $classMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedClassMetadataInterfaceProphecy->reveal()); - $classMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummyClassMetadataInterfaceProphecy->reveal()); + $classMetadataFactoryMock->method('getMetadataFor')->willReturnMap([ + [RelatedDummy::class, $relatedClassMetadataInterfaceMock], + [Dummy::class, $dummyClassMetadataInterfaceMock], + ]); - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->innerJoin(Argument::type('string'), Argument::type('string'))->shouldBeCalledTimes(2)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect(Argument::type('string'))->shouldBeCalled()->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->expects($this->exactly(2))->method('innerJoin')->with($this->isString(), $this->isString())->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->atLeastOnce())->method('addSelect')->with($this->isString())->willReturn($queryBuilderMock); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true, $classMetadataFactoryProphecy->reveal()); - $eagerExtensionTest->applyToCollection($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: ['enable_max_depth' => 'true', 'groups' => ['foo']])); + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true, $classMetadataFactoryMock); + $eagerExtensionTest->applyToCollection($queryBuilderMock, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: ['enable_max_depth' => 'true', 'groups' => ['foo']])); } public function testForceEager(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(UnknownDummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->with(UnknownDummy::class)->willReturn(new PropertyNameCollection(['id'])); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); $idPropertyMetadata = new ApiProperty(); $idPropertyMetadata = $idPropertyMetadata->withIdentifier(true); - $propertyMetadataFactoryProphecy->create(UnknownDummy::class, 'id', ['serializer_groups' => ['foobar'], 'normalization_groups' => 'foobar'])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relation', ['serializer_groups' => ['foobar'], 'normalization_groups' => 'foobar'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [UnknownDummy::class, 'id', ['serializer_groups' => ['foobar'], 'normalization_groups' => 'foobar'], $idPropertyMetadata], + [Dummy::class, 'relation', ['serializer_groups' => ['foobar'], 'normalization_groups' => 'foobar'], $relationPropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [new JoinColumn(nullable: false)]], ]; - $unknownClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $unknownClassMetadataProphecy->associationMappings = []; + $unknownClassMetadataMock = $this->createMock(ClassMetadata::class); + $unknownClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(UnknownDummy::class)->shouldBeCalled()->willReturn($unknownClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [UnknownDummy::class, $unknownClassMetadataMock], + ]); - $queryBuilderProphecy->innerJoin('o.relation', 'relation_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relation_a1.{id}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->expects($this->exactly(1))->method('innerJoin')->with('o.relation', 'relation_a1')->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->exactly(1))->method('addSelect')->with('partial relation_a1.{id}')->willReturn($queryBuilderMock); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['join', []], + ['select', []], + ]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, true, true); - $orderExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foobar'])); + $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, true, true); + $orderExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foobar'])); } public function testExtraLazy(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relation', ['serializer_groups' => ['foobar'], 'normalization_groups' => 'foobar'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->with(Dummy::class, 'relation', ['serializer_groups' => ['foobar'], 'normalization_groups' => 'foobar'])->willReturn($relationPropertyMetadata); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relation' => ['fetch' => ClassMetadata::FETCH_EXTRA_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [['nullable' => false]]], ]; - $unknownClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $unknownClassMetadataProphecy->associationMappings = []; + $unknownClassMetadataMock = $this->createMock(ClassMetadata::class); + $unknownClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, true, true); - $orderExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foobar'])); + $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, true, true); + $orderExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foobar'])); } public function testResourceClassNotFoundException(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relation', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willThrow(new ResourceClassNotFoundException()); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock->method('create')->with(Dummy::class, 'relation', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willThrowException(new ResourceClassNotFoundException()); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [['nullable' => false]]], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - - $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, true, true); - $orderExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, true, true); + $orderExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } public function testPropertyNotFoundException(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relation', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willThrow(new PropertyNotFoundException()); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock->method('create')->with(Dummy::class, 'relation', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willThrowException(new PropertyNotFoundException()); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [['nullable' => false]]], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - - $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, true, true); - $orderExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, true, true); + $orderExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } public function testResourceClassNotFoundExceptionPropertyNameCollection(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(UnknownDummy::class)->willThrow(new ResourceClassNotFoundException()); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock->method('create')->with(UnknownDummy::class)->willThrowException(new ResourceClassNotFoundException()); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relation', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock->method('create')->with(Dummy::class, 'relation', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [new JoinColumn(nullable: false)]], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(UnknownDummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - $queryBuilderProphecy->innerJoin('o.relation', 'relation_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); - - $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, true, true); - $orderExtensionTest->applyToItem($queryBuilderProphecy->reveal(), new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->exactly(2))->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [UnknownDummy::class, $classMetadataMock], + ]); + $queryBuilderMock = $this->createMock(QueryBuilder::class); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + $queryBuilderMock->expects($this->exactly(1))->method('innerJoin')->with('o.relation', 'relation_a1')->willReturn($queryBuilderMock); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); + + $orderExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, true, true); + $orderExtensionTest->applyToItem($queryBuilderMock, new QueryNameGenerator(), Dummy::class, [], new Get(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } public function testAttributes(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['id', 'name']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->with(RelatedDummy::class)->willReturn($relatedNameCollection); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $idPropertyMetadata = new ApiProperty(); $idPropertyMetadata = $idPropertyMetadata->withIdentifier(true); $namePropertyMetadata = new ApiProperty(); $namePropertyMetadata = $namePropertyMetadata->withReadable(true); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($namePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [RelatedDummy::class, 'id', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $idPropertyMetadata], + [RelatedDummy::class, 'name', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $namePropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummies' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $hasFieldMap = []; foreach ($relatedNameCollection as $property) { if ('id' !== $property) { - $relatedClassMetadataProphecy->hasField($property)->willReturn(true)->shouldBeCalled(); + $hasFieldMap[] = [$property, true]; } } + $relatedClassMetadataMock->expects($this->atLeastOnce())->method('hasField')->willReturnMap($hasFieldMap); - $relatedClassMetadataProphecy->associationMappings = []; + $relatedClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->leftJoin('o.relatedDummies', 'relatedDummies_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummies_a1.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy_a2.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->expects($this->exactly(2))->method('leftJoin')->willReturnMap([ + ['o.relatedDummies', 'relatedDummies_a1', $queryBuilderMock], + ['o.relatedDummy', 'relatedDummy_a2', $queryBuilderMock], + ]); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(2))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + + $this->assertSame([ + 'partial relatedDummies_a1.{id,name}', + 'partial relatedDummy_a2.{id,name}', + ], $actualSelects); } public function testNotInAttributes(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->with(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => 3, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $relatedClassMetadataProphecy->associationMappings = []; + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $relatedClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo', AbstractNormalizer::ATTRIBUTES => ['relatedDummy']])); } public function testOnlyOneRelationNotInAttributes(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); $relatedNameCollection = new PropertyNameCollection(['id', 'name']); - $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryMock->expects($this->atLeastOnce())->method('create')->with(RelatedDummy::class)->willReturn($relatedNameCollection); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $idPropertyMetadata = new ApiProperty(); $idPropertyMetadata = $idPropertyMetadata->withIdentifier(true); $namePropertyMetadata = new ApiProperty(); $namePropertyMetadata = $namePropertyMetadata->withReadable(true); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'id', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($idPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'name', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($namePropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummies', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [RelatedDummy::class, 'id', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $idPropertyMetadata], + [RelatedDummy::class, 'name', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $namePropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummies' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $hasFieldMap = []; foreach ($relatedNameCollection as $property) { if ('id' !== $property) { - $relatedClassMetadataProphecy->hasField($property)->willReturn(true)->shouldBeCalled(); + $hasFieldMap[] = [$property, true]; } } + $relatedClassMetadataMock->expects($this->atLeastOnce())->method('hasField')->willReturnMap($hasFieldMap); - $relatedClassMetadataProphecy->associationMappings = []; + $relatedClassMetadataMock->associationMappings = []; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->exactly(2))->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name}')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); + $queryBuilderMock->expects($this->exactly(1))->method('leftJoin')->with('o.relatedDummy', 'relatedDummy_a1')->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->exactly(1))->method('addSelect')->with('partial relatedDummy_a1.{id,name}')->willReturn($queryBuilderMock); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ]); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false, true); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo', AbstractNormalizer::ATTRIBUTES => ['relatedDummy' => ['id', 'name']]])); } public function testApplyToCollectionNoPartial(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $relatedClassMetadataProphecy->associationMappings = []; - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); - - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('relatedDummy2_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30); + $emMock = $this->createMock(EntityManagerInterface::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $relatedClassMetadataMock->associationMappings = []; + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); + + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $queryBuilderMock->expects($this->exactly(1))->method('leftJoin')->with('o.relatedDummy', 'relatedDummy_a1')->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->exactly(1))->method('innerJoin')->with('o.relatedDummy2', 'relatedDummy2_a2')->willReturn($queryBuilderMock); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(2))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ['select', []], + ]); + + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + + $this->assertSame(['relatedDummy_a1', 'relatedDummy2_a2'], $actualSelects); } public function testApplyToCollectionWithANonReadableButFetchEagerProperty(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withFetchEager(true); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false); @@ -968,37 +1109,50 @@ public function testApplyToCollectionWithANonReadableButFetchEagerProperty(): vo $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(false); $relationPropertyMetadata = $relationPropertyMetadata->withReadable(false); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); - $relatedClassMetadataProphecy->associationMappings = []; - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); - - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('relatedDummy_a1')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->addSelect('relatedDummy2_a2')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([]); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30); + $emMock = $this->createMock(EntityManagerInterface::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); + $relatedClassMetadataMock->associationMappings = []; + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); + + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + + $queryBuilderMock->expects($this->exactly(1))->method('leftJoin')->with('o.relatedDummy', 'relatedDummy_a1')->willReturn($queryBuilderMock); + $queryBuilderMock->expects($this->exactly(1))->method('innerJoin')->with('o.relatedDummy2', 'relatedDummy2_a2')->willReturn($queryBuilderMock); + $actualSelects = []; + $queryBuilderMock->expects($this->exactly(2))->method('addSelect') + ->willReturnCallback(static function (string $select) use (&$actualSelects, $queryBuilderMock): QueryBuilder { + $actualSelects[] = $select; + + return $queryBuilderMock; + }); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', []], + ['select', []], + ]); + + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + + $this->assertSame(['relatedDummy_a1', 'relatedDummy2_a2'], $actualSelects); } #[DataProvider('provideExistingJoinCases')] @@ -1007,40 +1161,40 @@ public function testApplyToCollectionWithExistingJoin(string $joinType): void $context = ['groups' => ['foo']]; $callContext = ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo']; - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->with(Dummy::class, 'relatedDummy', $callContext)->willReturn($relationPropertyMetadata); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], ]; - $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $relatedClassMetadataMock = $this->createMock(ClassMetadata::class); - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->exactly(2))->method('getClassMetadata')->willReturnMap([ + [Dummy::class, $classMetadataMock], + [RelatedDummy::class, $relatedClassMetadataMock], + ]); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); - $queryBuilderProphecy->getDQLPart('join')->willReturn([ - 'o' => [ - new Join($joinType, 'o.relatedDummy', 'existing_join_alias'), - ], + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); + $queryBuilderMock->method('getDQLPart')->willReturnMap([ + ['select', []], + ['join', ['o' => [new Join($joinType, 'o.relatedDummy', 'existing_join_alias')]]], + ['select', []], ]); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->addSelect('existing_join_alias')->shouldBeCalledTimes(1)->willReturn($queryBuilderProphecy); + $queryBuilderMock->expects($this->exactly(1))->method('addSelect')->with('existing_join_alias')->willReturn($queryBuilderMock); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30, false); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo']), $context); } @@ -1052,76 +1206,75 @@ public static function provideExistingJoinCases(): iterable public function testApplyToCollectionWithAReadableButNotFetchEagerProperty(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withFetchEager(false); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); $relationPropertyMetadata = $relationPropertyMetadata->withReadable(true); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->willReturnMap([ + [Dummy::class, 'relatedDummy', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + [Dummy::class, 'relatedDummy2', ['serializer_groups' => ['foo'], 'normalization_groups' => 'foo'], $relationPropertyMetadata], + ]); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(RelatedDummy::class)->shouldNotBecalled(); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(Dummy::class)->willReturn($classMetadataMock); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldNotBeCalled(); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a2')->shouldNotBeCalled(); - $queryBuilderProphecy->addSelect('relatedDummy_a1')->shouldNotBeCalled(); - $queryBuilderProphecy->addSelect('relatedDummy2_a2')->shouldNotBeCalled(); + $queryBuilderMock->expects($this->never())->method('leftJoin')->with('o.relatedDummy', 'relatedDummy_a1'); + $queryBuilderMock->expects($this->never())->method('innerJoin')->with('o.relatedDummy2', 'relatedDummy2_a2'); + $queryBuilderMock->expects($this->never())->method('addSelect'); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } public function testAvoidFetchCollectionOnIriOnlyProperty(): void { - $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryMock = $this->createMock(PropertyNameCollectionFactoryInterface::class); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryMock = $this->createMock(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); $relationPropertyMetadata = $relationPropertyMetadata->withFetchEager(true); $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); $relationPropertyMetadata = $relationPropertyMetadata->withReadable(true); $relationPropertyMetadata = $relationPropertyMetadata->withUriTemplate('/property-collection-relations'); - $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ['serializer_groups' => ['read'], 'normalization_groups' => 'read'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryMock->expects($this->atLeastOnce())->method('create')->with(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ['serializer_groups' => ['read'], 'normalization_groups' => 'read'])->willReturn($relationPropertyMetadata); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + $queryBuilderMock = $this->createMock(QueryBuilder::class); - $classMetadataProphecy = $this->prophesize(ClassMetadata::class); - $classMetadataProphecy->associationMappings = [ + $classMetadataMock = $this->createMock(ClassMetadata::class); + $classMetadataMock->associationMappings = [ 'propertyCollectionIriOnlyRelation' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => PropertyCollectionIriOnlyRelation::class], ]; - $emProphecy = $this->prophesize(EntityManager::class); - $emProphecy->getClassMetadata(PropertyCollectionIriOnly::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); - $emProphecy->getClassMetadata(PropertyCollectionIriOnlyRelation::class)->shouldNotBecalled(); + $emMock = $this->createMock(EntityManagerInterface::class); + $emMock->expects($this->atLeastOnce())->method('getClassMetadata')->with(PropertyCollectionIriOnly::class)->willReturn($classMetadataMock); - $queryBuilderProphecy->getRootAliases()->willReturn(['o']); - $queryBuilderProphecy->getDQLPart('select')->willReturn([]); - $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + $queryBuilderMock->method('getRootAliases')->willReturn(['o']); + $queryBuilderMock->method('getDQLPart')->with('select')->willReturn([]); + $queryBuilderMock->method('getEntityManager')->willReturn($emMock); - $queryBuilderProphecy->leftJoin('o.propertyCollectionIriOnlyRelation', 'propertyCollectionIriOnlyRelation_a1')->shouldNotBeCalled(); - $queryBuilderProphecy->addSelect('propertyCollectionIriOnlyRelation_a1')->shouldNotBeCalled(); + $queryBuilderMock->expects($this->never())->method('leftJoin')->with('o.propertyCollectionIriOnlyRelation', 'propertyCollectionIriOnlyRelation_a1'); + $queryBuilderMock->expects($this->never())->method('addSelect')->with('propertyCollectionIriOnlyRelation_a1'); - $queryBuilder = $queryBuilderProphecy->reveal(); - $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30); + $queryBuilder = $queryBuilderMock; + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryMock, $propertyMetadataFactoryMock, 30); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), PropertyCollectionIriOnly::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'read'])); } } From 8a8d6aa03493d58c9b09653a3829c03b0140173a Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 15 Sep 2026 17:50:28 +0200 Subject: [PATCH 3/4] revert(doctrine): restore the class_exists guard Removing the guard reproducibly broke ContextSwitchTest's partial-fetch cases on the Symfony lowest job (green on 4.4 across six runs, red here across two), even though that job installs doctrine/orm 3.7.1 where PartialObjectExpression exists and the guard should be inert. The mechanism is not understood yet. The floors are the actual fix for the doctrine-orm lowest job; dropping dead code is cosmetic and can wait until the interaction is explained. --- src/Doctrine/Orm/Extension/EagerLoadingExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index d68afc91b7d..01a54bcedc1 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -23,6 +23,7 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query\AST\PartialObjectExpression; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Select; use Doctrine\ORM\QueryBuilder; @@ -70,7 +71,7 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $ $options = []; $forceEager = $operation?->getForceEager() ?? $this->forceEager; - $fetchPartial = $operation?->getFetchPartial() ?? $this->fetchPartial; + $fetchPartial = class_exists(PartialObjectExpression::class) && ($operation?->getFetchPartial() ?? $this->fetchPartial); if (!isset($context['groups']) && !isset($context['attributes'])) { $contextType = isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context'; From ee04855987d6dd71e27ab8399d596ef69b59a256 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 15 Sep 2026 17:54:16 +0200 Subject: [PATCH 4/4] fix(doctrine): raise orm floor in the root too eea6fc84f bumped only src/Doctrine/Orm/composer.json, but the root suite installs from the root composer.json, where --prefer-lowest still resolved doctrine/orm 3.0.1. The PARTIAL DQL grammar does not exist there, so dropping the class_exists() guard made the extension emit partial selects that the parser rejects, breaking ContextSwitchTest. Bump the root floors to match the component and restore the guard removal. --- composer.json | 4 ++-- src/Doctrine/Orm/Extension/EagerLoadingExtension.php | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8eff65c3f34..959a6a38d2b 100644 --- a/composer.json +++ b/composer.json @@ -128,8 +128,8 @@ "require-dev": { "doctrine/common": "^3.2.2", "doctrine/dbal": "^4.0", - "doctrine/doctrine-bundle": "^2.11 || ^3.1", - "doctrine/orm": "^2.17 || ^3.0", + "doctrine/doctrine-bundle": "^2.11.1 || ^3.1", + "doctrine/orm": "^2.17 || ^3.3", "elasticsearch/elasticsearch": "^7.17 || ^8.4 || ^9.0", "friendsofphp/php-cs-fixer": "^3.93", "guzzlehttp/guzzle": "^6.0 || ^7.0", diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index 01a54bcedc1..d68afc91b7d 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -23,7 +23,6 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Query\AST\PartialObjectExpression; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Select; use Doctrine\ORM\QueryBuilder; @@ -71,7 +70,7 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $ $options = []; $forceEager = $operation?->getForceEager() ?? $this->forceEager; - $fetchPartial = class_exists(PartialObjectExpression::class) && ($operation?->getFetchPartial() ?? $this->fetchPartial); + $fetchPartial = $operation?->getFetchPartial() ?? $this->fetchPartial; if (!isset($context['groups']) && !isset($context['attributes'])) { $contextType = isset($context['api_denormalize']) ? 'denormalization_context' : 'normalization_context';