25
25
use Doctrine \ORM \Mapping \ClassMetadataInfo ;
26
26
use Doctrine \ORM \QueryBuilder ;
27
27
use Symfony \Component \HttpFoundation \RequestStack ;
28
+ use Symfony \Component \Serializer \Mapping \Factory \ClassMetadataFactoryInterface ;
28
29
29
30
/**
30
31
* Eager loads relations.
@@ -40,18 +41,20 @@ final class EagerLoadingExtension implements QueryCollectionExtensionInterface,
40
41
41
42
private $ propertyNameCollectionFactory ;
42
43
private $ propertyMetadataFactory ;
44
+ private $ classMetadataFactory ;
43
45
private $ maxJoins ;
44
46
private $ serializerContextBuilder ;
45
47
private $ requestStack ;
46
48
47
49
/**
48
50
* @TODO move $fetchPartial after $forceEager (@soyuka) in 3.0
49
51
*/
50
- public function __construct (PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory , ResourceMetadataFactoryInterface $ resourceMetadataFactory , int $ maxJoins = 30 , bool $ forceEager = true , RequestStack $ requestStack = null , SerializerContextBuilderInterface $ serializerContextBuilder = null , bool $ fetchPartial = false )
52
+ public function __construct (PropertyNameCollectionFactoryInterface $ propertyNameCollectionFactory , PropertyMetadataFactoryInterface $ propertyMetadataFactory , ResourceMetadataFactoryInterface $ resourceMetadataFactory , int $ maxJoins = 30 , bool $ forceEager = true , RequestStack $ requestStack = null , SerializerContextBuilderInterface $ serializerContextBuilder = null , bool $ fetchPartial = false , ClassMetadataFactoryInterface $ classMetadataFactory = null )
51
53
{
52
54
$ this ->propertyNameCollectionFactory = $ propertyNameCollectionFactory ;
53
55
$ this ->propertyMetadataFactory = $ propertyMetadataFactory ;
54
56
$ this ->resourceMetadataFactory = $ resourceMetadataFactory ;
57
+ $ this ->classMetadataFactory = $ classMetadataFactory ;
55
58
$ this ->maxJoins = $ maxJoins ;
56
59
$ this ->forceEager = $ forceEager ;
57
60
$ this ->fetchPartial = $ fetchPartial ;
@@ -72,10 +75,11 @@ public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGenerator
72
75
73
76
$ forceEager = $ this ->shouldOperationForceEager ($ resourceClass , $ options );
74
77
$ fetchPartial = $ this ->shouldOperationFetchPartial ($ resourceClass , $ options );
78
+ $ serializerContext = $ this ->getSerializerContext ($ resourceClass , 'normalization_context ' , $ options );
75
79
76
- $ groups = $ this ->getSerializerGroups ($ resourceClass , $ options , ' normalization_context ' );
80
+ $ groups = $ this ->getSerializerGroups ($ options , $ serializerContext );
77
81
78
- $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ resourceClass , $ forceEager , $ fetchPartial , $ queryBuilder ->getRootAliases ()[0 ], $ groups );
82
+ $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ resourceClass , $ forceEager , $ fetchPartial , $ queryBuilder ->getRootAliases ()[0 ], $ groups, $ serializerContext );
79
83
}
80
84
81
85
/**
@@ -92,16 +96,16 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
92
96
93
97
$ forceEager = $ this ->shouldOperationForceEager ($ resourceClass , $ options );
94
98
$ fetchPartial = $ this ->shouldOperationFetchPartial ($ resourceClass , $ options );
99
+ $ contextType = isset ($ context ['api_denormalize ' ]) ? 'denormalization_context ' : 'normalization_context ' ;
100
+ $ serializerContext = $ this ->getSerializerContext ($ context ['resource_class ' ] ?? $ resourceClass , $ contextType , $ options );
95
101
96
102
if (isset ($ context ['groups ' ])) {
97
103
$ groups = ['serializer_groups ' => $ context ['groups ' ]];
98
- } elseif (isset ($ context ['resource_class ' ])) {
99
- $ groups = $ this ->getSerializerGroups ($ context ['resource_class ' ], $ options , isset ($ context ['api_denormalize ' ]) ? 'denormalization_context ' : 'normalization_context ' );
100
104
} else {
101
- $ groups = $ this ->getSerializerGroups ($ resourceClass , $ options , ' normalization_context ' );
105
+ $ groups = $ this ->getSerializerGroups ($ options , $ serializerContext );
102
106
}
103
107
104
- $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ resourceClass , $ forceEager , $ fetchPartial , $ queryBuilder ->getRootAliases ()[0 ], $ groups );
108
+ $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ resourceClass , $ forceEager , $ fetchPartial , $ queryBuilder ->getRootAliases ()[0 ], $ groups, $ serializerContext );
105
109
}
106
110
107
111
/**
@@ -113,21 +117,30 @@ public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterf
113
117
* @param bool $forceEager
114
118
* @param string $parentAlias
115
119
* @param array $propertyMetadataOptions
120
+ * @param array $context
116
121
* @param bool $wasLeftJoin if the relation containing the new one had a left join, we have to force the new one to left join too
117
122
* @param int $joinCount the number of joins
123
+ * @param int $currentDepth the current max depth
118
124
*
119
125
* @throws RuntimeException when the max number of joins has been reached
120
126
*/
121
- private function joinRelations (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , bool $ forceEager , bool $ fetchPartial , string $ parentAlias , array $ propertyMetadataOptions = [], bool $ wasLeftJoin = false , int &$ joinCount = 0 )
127
+ private function joinRelations (QueryBuilder $ queryBuilder , QueryNameGeneratorInterface $ queryNameGenerator , string $ resourceClass , bool $ forceEager , bool $ fetchPartial , string $ parentAlias , array $ propertyMetadataOptions = [], array $ context = [], bool $ wasLeftJoin = false , int &$ joinCount = 0 , int $ currentDepth = null )
122
128
{
123
129
if ($ joinCount > $ this ->maxJoins ) {
124
- throw new RuntimeException ('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary. ' );
130
+ throw new RuntimeException ('The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary, or use the "max_depth" option of the Symfony serializer . ' );
125
131
}
126
132
133
+ $ currentDepth = $ currentDepth > 0 ? $ currentDepth - 1 : $ currentDepth ;
127
134
$ entityManager = $ queryBuilder ->getEntityManager ();
128
135
$ classMetadata = $ entityManager ->getClassMetadata ($ resourceClass );
136
+ $ attributesMetadata = $ this ->classMetadataFactory ? $ this ->classMetadataFactory ->getMetadataFor ($ resourceClass )->getAttributesMetadata () : null ;
129
137
130
138
foreach ($ classMetadata ->associationMappings as $ association => $ mapping ) {
139
+ //Don't join if max depth is enabled and the current depth limit is reached
140
+ if (isset ($ context ['enable_max_depth ' ]) && 0 === $ currentDepth ) {
141
+ continue ;
142
+ }
143
+
131
144
try {
132
145
$ propertyMetadata = $ this ->propertyMetadataFactory ->create ($ resourceClass , $ association , $ propertyMetadataOptions );
133
146
} catch (PropertyNotFoundException $ propertyNotFoundException ) {
@@ -174,7 +187,16 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt
174
187
continue ;
175
188
}
176
189
177
- $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ mapping ['targetEntity ' ], $ forceEager , $ fetchPartial , $ associationAlias , $ propertyMetadataOptions , $ method === 'leftJoin ' , $ joinCount );
190
+ if (isset ($ attributesMetadata [$ association ])) {
191
+ $ maxDepth = $ attributesMetadata [$ association ]->getMaxDepth ();
192
+
193
+ // The current depth is the lowest max depth available in the ancestor tree.
194
+ if (null !== $ maxDepth && (null === $ currentDepth || $ maxDepth < $ currentDepth )) {
195
+ $ currentDepth = $ maxDepth ;
196
+ }
197
+ }
198
+
199
+ $ this ->joinRelations ($ queryBuilder , $ queryNameGenerator , $ mapping ['targetEntity ' ], $ forceEager , $ fetchPartial , $ associationAlias , $ propertyMetadataOptions , $ context , $ method === 'leftJoin ' , $ joinCount , $ currentDepth );
178
200
}
179
201
}
180
202
@@ -215,15 +237,15 @@ private function addSelect(QueryBuilder $queryBuilder, string $entity, string $a
215
237
}
216
238
217
239
/**
218
- * Gets serializer groups if available, if not it returns the $options array .
240
+ * Gets serializer context .
219
241
*
220
242
* @param string $resourceClass
243
+ * @param string $contextType normalization_context or denormalization_context
221
244
* @param array $options represents the operation name so that groups are the one of the specific operation
222
- * @param string $context normalization_context or denormalization_context
223
245
*
224
246
* @return array
225
247
*/
226
- private function getSerializerGroups (string $ resourceClass , array $ options , string $ context ): array
248
+ private function getSerializerContext (string $ resourceClass , string $ contextType , array $ options ): array
227
249
{
228
250
$ request = null ;
229
251
@@ -232,23 +254,32 @@ private function getSerializerGroups(string $resourceClass, array $options, stri
232
254
}
233
255
234
256
if (null !== $ this ->serializerContextBuilder && null !== $ request ) {
235
- $ contextFromRequest = $ this ->serializerContextBuilder ->createFromRequest ($ request , $ context === 'normalization_context ' );
236
-
237
- if (isset ($ contextFromRequest ['groups ' ])) {
238
- return ['serializer_groups ' => $ contextFromRequest ['groups ' ]];
239
- }
257
+ return $ this ->serializerContextBuilder ->createFromRequest ($ request , 'normalization_context ' === $ contextType );
240
258
}
241
259
242
260
$ resourceMetadata = $ this ->resourceMetadataFactory ->create ($ resourceClass );
243
261
244
262
if (isset ($ options ['collection_operation_name ' ])) {
245
- $ context = $ resourceMetadata ->getCollectionOperationAttribute ($ options ['collection_operation_name ' ], $ context , null , true );
263
+ $ context = $ resourceMetadata ->getCollectionOperationAttribute ($ options ['collection_operation_name ' ], $ contextType , null , true );
246
264
} elseif (isset ($ options ['item_operation_name ' ])) {
247
- $ context = $ resourceMetadata ->getItemOperationAttribute ($ options ['item_operation_name ' ], $ context , null , true );
265
+ $ context = $ resourceMetadata ->getItemOperationAttribute ($ options ['item_operation_name ' ], $ contextType , null , true );
248
266
} else {
249
- $ context = $ resourceMetadata ->getAttribute ($ context );
267
+ $ context = $ resourceMetadata ->getAttribute ($ contextType );
250
268
}
251
269
270
+ return $ context ? $ context : [];
271
+ }
272
+
273
+ /**
274
+ * Gets serializer groups if available, if not it returns the $options array.
275
+ *
276
+ * @param array $options represents the operation name so that groups are the one of the specific operation
277
+ * @param array $context
278
+ *
279
+ * @return array
280
+ */
281
+ private function getSerializerGroups (array $ options , array $ context ): array
282
+ {
252
283
if (empty ($ context ['groups ' ])) {
253
284
return $ options ;
254
285
}
0 commit comments