diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index bcff2458125..536f6a4dc1c 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -26,6 +26,7 @@ use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Select; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; @@ -198,11 +199,13 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt if (true === $fetchPartial) { try { - $this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $options); + $propertyOptions = $this->getPropertyContext($attributesMetadata[$association] ?? null, $options); + $this->addSelect($queryBuilder, $mapping['targetEntity'], $associationAlias, $propertyOptions); } catch (ResourceClassNotFoundException) { continue; } } else { + $propertyOptions = null; $this->addSelectOnce($queryBuilder, $associationAlias); } @@ -225,10 +228,45 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt } } - $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $options, $childNormalizationContext, $isLeftJoin, $joinCount, $currentDepth, $association); + $propertyOptions ??= $this->getPropertyContext($attributesMetadata[$association] ?? null, $options); + $this->joinRelations($queryBuilder, $queryNameGenerator, $mapping['targetEntity'], $forceEager, $fetchPartial, $associationAlias, $propertyOptions, $childNormalizationContext, $isLeftJoin, $joinCount, $currentDepth, $association); } } + private function getPropertyContext(?AttributeMetadataInterface $attributeMetadata, array $options): array + { + if (null === $attributeMetadata) { + return $options; + } + + $hasNormalizationContext = (isset($options['normalization_groups']) || isset($options['serializer_groups'])) && [] !== $attributeMetadata->getNormalizationContexts(); + $hasDenormalizationContext = (isset($options['denormalization_groups']) || isset($options['serializer_groups'])) && [] !== $attributeMetadata->getDenormalizationContexts(); + + if (!$hasNormalizationContext && !$hasDenormalizationContext) { + return $options; + } + + $propertyOptions = $options; + $propertyOptions['normalization_groups'] ??= $options['serializer_groups']; + $propertyOptions['denormalization_groups'] ??= $options['serializer_groups']; + // we don't rely on 'serializer_groups' anymore because `context` changes normalization and/or denormalization + // but does not have options for both at the same time + unset($propertyOptions['serializer_groups']); + + if ($hasNormalizationContext) { + $originalGroups = $options['normalization_groups'] ?? $options['serializer_groups']; + $propertyContext = $attributeMetadata->getNormalizationContextForGroups((array) $originalGroups); + $propertyOptions['normalization_groups'] = $propertyContext[AbstractNormalizer::GROUPS] ?? $originalGroups; + } + if ($hasDenormalizationContext) { + $originalGroups = $options['denormalization_groups'] ?? $options['serializer_groups']; + $propertyContext = $attributeMetadata->getDenormalizationContextForGroups((array) $originalGroups); + $propertyOptions['denormalization_groups'] = $propertyContext[AbstractNormalizer::GROUPS] ?? $originalGroups; + } + + return $propertyOptions; + } + private function addSelect(QueryBuilder $queryBuilder, string $entity, string $associationAlias, array $propertyMetadataOptions): void { $select = []; diff --git a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php index 2ad7a74d335..73f079db0d4 100644 --- a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php +++ b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php @@ -336,6 +336,82 @@ public function testDenormalizeItemWithExistingGroups(): void $eagerExtensionTest->applyToItem($queryBuilderProphecy->reveal(), 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); + + $relatedNameCollection = new PropertyNameCollection(['id', 'name']); + $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); + + $propertyMetadataFactoryProphecy = $this->prophesize(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(); + + $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + + $classMetadataProphecy = $this->prophesize(ClassMetadata::class); + $classMetadataProphecy->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); + + foreach ($relatedNameCollection as $property) { + if ('id' !== $property && 'embeddedDummy' !== $property) { + $relatedClassMetadataProphecy->hasField($property)->willReturn('notindatabase' !== $property)->shouldBeCalled(); + } + } + + $dummyClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); + $relatedClassMetadataInterfaceProphecy = $this->prophesize(ClassMetadataInterface::class); + $classMetadataFactoryProphecy = $this->prophesize(ClassMetadataFactoryInterface::class); + + $relatedDummyAttributeMetadata = new AttributeMetadata('relatedDummy'); + $relatedDummyAttributeMetadata->setNormalizationContextForGroups(['groups' => ['bar']], ['foo']); + + $dummyClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn(['relatedDummy' => $relatedDummyAttributeMetadata]); + $relatedClassMetadataInterfaceProphecy->getAttributesMetadata()->willReturn([]); + + $classMetadataFactoryProphecy->getMetadataFor(RelatedDummy::class)->willReturn($relatedClassMetadataInterfaceProphecy->reveal()); + $classMetadataFactoryProphecy->getMetadataFor(Dummy::class)->willReturn($dummyClassMetadataInterfaceProphecy->reveal()); + + $relatedClassMetadataProphecy->associationMappings = []; + + $emProphecy = $this->prophesize(EntityManager::class); + $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); + $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); + + $queryBuilderProphecy->getRootAliases()->willReturn(['o']); + $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + + $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([]); + + $queryBuilder = $queryBuilderProphecy->reveal(); + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30, false, true, $classMetadataFactoryProphecy->reveal()); + $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); + } + public function testMaxJoinsReached(): void { $this->expectException(RuntimeException::class); @@ -496,7 +572,6 @@ public function testForceEager(): void public function testExtraLazy(): void { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - // $propertyNameCollectionFactoryProphecy->create(UnknownDummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty();