2323use ApiPlatform \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
2424use ApiPlatform \Metadata \Property \Factory \PropertyNameCollectionFactoryInterface ;
2525use Doctrine \ORM \Mapping \ClassMetadata ;
26+ use Doctrine \ORM \Query \AST \PartialObjectExpression ;
2627use Doctrine \ORM \Query \Expr \Join ;
2728use Doctrine \ORM \Query \Expr \Select ;
2829use Doctrine \ORM \QueryBuilder ;
@@ -70,7 +71,7 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $
7071 $ options = [];
7172
7273 $ forceEager = $ operation ?->getForceEager() ?? $ this ->forceEager ;
73- $ fetchPartial = $ operation ?->getFetchPartial() ?? $ this ->fetchPartial ;
74+ $ fetchPartial = class_exists (PartialObjectExpression::class) && ( $ operation ?->getFetchPartial() ?? $ this ->fetchPartial ) ;
7475
7576 if (!isset ($ context ['groups ' ]) && !isset ($ context ['attributes ' ])) {
7677 $ contextType = isset ($ context ['api_denormalize ' ]) ? 'denormalization_context ' : 'normalization_context ' ;
@@ -95,7 +96,61 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $
9596 $ options ['denormalization_groups ' ] = $ denormalizationGroups ;
9697 }
9798
98- $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ resourceClass , $ forceEager , $ fetchPartial , $ queryBuilder ->getRootAliases ()[0 ], $ options , $ context );
99+ $ selects = $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ resourceClass , $ forceEager , $ fetchPartial , $ queryBuilder ->getRootAliases ()[0 ], $ options , $ context );
100+ $ selectsByClass = [];
101+ foreach ($ selects as [$ entity , $ alias , $ fields ]) {
102+ if ($ entity === $ resourceClass ) {
103+ // We don't perform partial select the root entity
104+ $ fields = null ;
105+ }
106+
107+ if (!isset ($ selectsByClass [$ entity ])) {
108+ $ selectsByClass [$ entity ] = [
109+ 'aliases ' => [$ alias => true ],
110+ 'fields ' => null === $ fields ? null : array_flip ($ fields ),
111+ ];
112+ } else {
113+ $ selectsByClass [$ entity ]['aliases ' ][$ alias ] = true ;
114+ if (null === $ selectsByClass [$ entity ]['fields ' ]) {
115+ continue ;
116+ }
117+
118+ if (null === $ fields ) {
119+ $ selectsByClass [$ entity ]['fields ' ] = null ;
120+ continue ;
121+ }
122+
123+ // Merge fields
124+ foreach ($ fields as $ field ) {
125+ $ selectsByClass [$ entity ]['fields ' ][$ field ] = true ;
126+ }
127+ }
128+ }
129+
130+ $ existingSelects = [];
131+ foreach ($ queryBuilder ->getDQLPart ('select ' ) ?? [] as $ dqlSelect ) {
132+ if (!$ dqlSelect instanceof Select) {
133+ continue ;
134+ }
135+ foreach ($ dqlSelect ->getParts () as $ part ) {
136+ $ existingSelects [(string ) $ part ] = true ;
137+ }
138+ }
139+
140+ foreach ($ selectsByClass as $ data ) {
141+ $ fields = null === $ data ['fields ' ] ? null : array_keys ($ data ['fields ' ]);
142+ foreach (array_keys ($ data ['aliases ' ]) as $ alias ) {
143+ if (isset ($ existingSelects [$ alias ])) {
144+ continue ;
145+ }
146+
147+ if (null === $ fields ) {
148+ $ queryBuilder ->addSelect ($ alias );
149+ } else {
150+ $ queryBuilder ->addSelect (\sprintf ('partial %s.{%s} ' , $ alias , implode (', ' , $ fields )));
151+ }
152+ }
153+ }
99154 }
100155
101156 /**
@@ -107,7 +162,7 @@ private function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $
107162 *
108163 * @throws RuntimeException when the max number of joins has been reached
109164 */
110- private function joinRelations (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , bool $ forceEager , bool $ fetchPartial , string $ parentAlias , array $ options = [], array $ normalizationContext = [], bool $ wasLeftJoin = false , int &$ joinCount = 0 , ?int $ currentDepth = null , ?string $ parentAssociation = null ): void
165+ private function joinRelations (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , bool $ forceEager , bool $ fetchPartial , string $ parentAlias , array $ options = [], array $ normalizationContext = [], bool $ wasLeftJoin = false , int &$ joinCount = 0 , ?int $ currentDepth = null , ?string $ parentAssociation = null ): iterable
111166 {
112167 if ($ joinCount > $ this ->maxJoins ) {
113168 throw new RuntimeException ('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). ' );
@@ -200,13 +255,13 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
200255 if (true === $ fetchPartial ) {
201256 try {
202257 $ propertyOptions = $ this ->getPropertyContext ($ attributesMetadata [$ association ] ?? null , $ options );
203- $ this ->addSelect ($ queryBuilder , $ mapping ['targetEntity ' ], $ associationAlias , $ propertyOptions );
258+ yield from $ this ->addSelect ($ queryBuilder , $ mapping ['targetEntity ' ], $ associationAlias , $ propertyOptions );
204259 } catch (ResourceClassNotFoundException ) {
205260 continue ;
206261 }
207262 } else {
208263 $ propertyOptions = null ;
209- $ this -> addSelectOnce ( $ queryBuilder , $ associationAlias) ;
264+ yield [ $ resourceClass , $ associationAlias, null ] ;
210265 }
211266
212267 // Avoid recursive joins for self-referencing relations
@@ -229,7 +284,7 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
229284 }
230285
231286 $ propertyOptions ??= $ this ->getPropertyContext ($ attributesMetadata [$ association ] ?? null , $ options );
232- $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ mapping ['targetEntity ' ], $ forceEager , $ fetchPartial , $ associationAlias , $ propertyOptions , $ childNormalizationContext , $ isLeftJoin , $ joinCount , $ currentDepth , $ association );
287+ yield from $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ mapping ['targetEntity ' ], $ forceEager , $ fetchPartial , $ associationAlias , $ propertyOptions , $ childNormalizationContext , $ isLeftJoin , $ joinCount , $ currentDepth , $ association );
233288 }
234289 }
235290
@@ -267,13 +322,13 @@ private function getPropertyContext(?AttributeMetadataInterface $attributeMetada
267322 return $ propertyOptions ;
268323 }
269324
270- private function addSelect (QueryBuilder $ queryBuilder , string $ entity , string $ associationAlias , array $ propertyMetadataOptions ): void
325+ private function addSelect (QueryBuilder $ queryBuilder , string $ entity , string $ associationAlias , array $ propertyMetadataOptions ): iterable
271326 {
272327 $ select = [];
273328 $ entityManager = $ queryBuilder ->getEntityManager ();
274329 $ targetClassMetadata = $ entityManager ->getClassMetadata ($ entity );
275330 if (!empty ($ targetClassMetadata ->subClasses )) {
276- $ this -> addSelectOnce ( $ queryBuilder , $ associationAlias) ;
331+ yield [ $ entity , $ associationAlias, null ] ;
277332
278333 return ;
279334 }
@@ -308,15 +363,6 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a
308363 }
309364 }
310365
311- $ queryBuilder ->addSelect (\sprintf ('partial %s.{%s} ' , $ associationAlias , implode (', ' , $ select )));
312- }
313-
314- private function addSelectOnce (QueryBuilder $ queryBuilder , string $ alias ): void
315- {
316- $ existingSelects = array_reduce ($ queryBuilder ->getDQLPart ('select ' ) ?? [], fn ($ existing , $ dqlSelect ) => ($ dqlSelect instanceof Select) ? array_merge ($ existing , $ dqlSelect ->getParts ()) : $ existing , []);
317-
318- if (!\in_array ($ alias , $ existingSelects , true )) {
319- $ queryBuilder ->addSelect ($ alias );
320- }
366+ yield [$ entity , $ associationAlias , $ select ];
321367 }
322368}
0 commit comments