32
32
*/
33
33
final class TypeBuilder implements TypeBuilderInterface
34
34
{
35
+ public const INTERFACE_POSTFIX = 'Interface ' ;
36
+ public const ITEM_POSTFIX = 'Item ' ;
37
+ public const COLLECTION_POSTFIX = 'Collection ' ;
38
+ public const DATA_POSTFIX = 'Data ' ;
39
+
35
40
private $ typesContainer ;
36
41
private $ defaultFieldResolver ;
37
42
private $ fieldsBuilderLocator ;
@@ -53,6 +58,7 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadata $
53
58
if (null !== $ mutationName ) {
54
59
$ shortName = $ mutationName .ucfirst ($ shortName );
55
60
}
61
+
56
62
if ($ input ) {
57
63
$ shortName .= 'Input ' ;
58
64
} elseif (null !== $ mutationName ) {
@@ -61,70 +67,39 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadata $
61
67
}
62
68
$ shortName .= 'Payload ' ;
63
69
}
70
+
71
+ if ($ resourceMetadata ->getInterface ()) {
72
+ $ shortName .= self ::INTERFACE_POSTFIX ;
73
+ }
74
+
64
75
if ('item_query ' === $ queryName ) {
65
- $ shortName .= ' Item ' ;
76
+ $ shortName .= self :: ITEM_POSTFIX ;
66
77
}
78
+
67
79
if ('collection_query ' === $ queryName ) {
68
- $ shortName .= ' Collection ' ;
80
+ $ shortName .= self :: COLLECTION_POSTFIX ;
69
81
}
82
+
70
83
if ($ wrapped && null !== $ mutationName ) {
71
- $ shortName .= ' Data ' ;
84
+ $ shortName .= self :: DATA_POSTFIX ;
72
85
}
73
86
74
87
if ($ this ->typesContainer ->has ($ shortName )) {
75
88
$ resourceObjectType = $ this ->typesContainer ->get ($ shortName );
76
- if (!($ resourceObjectType instanceof ObjectType || $ resourceObjectType instanceof NonNull)) {
89
+ if (!($ resourceObjectType instanceof ObjectType || $ resourceObjectType instanceof NonNull || $ resourceObjectType instanceof InterfaceType )) {
77
90
throw new \UnexpectedValueException (sprintf (
78
91
'Expected GraphQL type "%s" to be %s. ' ,
79
92
$ shortName ,
80
- implode ('| ' , [ObjectType::class, NonNull::class])
93
+ implode ('| ' , [ObjectType::class, NonNull::class, InterfaceType::class ])
81
94
));
82
95
}
83
96
84
97
return $ resourceObjectType ;
85
98
}
86
99
87
- $ ioMetadata = $ resourceMetadata ->getGraphqlAttribute ($ mutationName ?? $ queryName , $ input ? 'input ' : 'output ' , null , true );
88
- if (null !== $ ioMetadata && \array_key_exists ('class ' , $ ioMetadata ) && null !== $ ioMetadata ['class ' ]) {
89
- $ resourceClass = $ ioMetadata ['class ' ];
90
- }
91
-
92
- $ wrapData = !$ wrapped && null !== $ mutationName && !$ input && $ depth < 1 ;
93
-
94
- $ configuration = [
95
- 'name ' => $ shortName ,
96
- 'description ' => $ resourceMetadata ->getDescription (),
97
- 'resolveField ' => $ this ->defaultFieldResolver ,
98
- 'fields ' => function () use ($ resourceClass , $ resourceMetadata , $ input , $ mutationName , $ queryName , $ wrapData , $ depth , $ ioMetadata ) {
99
- if ($ wrapData ) {
100
- $ queryNormalizationContext = $ resourceMetadata ->getGraphqlAttribute ($ queryName ?? '' , 'normalization_context ' , [], true );
101
- $ mutationNormalizationContext = $ resourceMetadata ->getGraphqlAttribute ($ mutationName ?? '' , 'normalization_context ' , [], true );
102
- // Use a new type for the wrapped object only if there is a specific normalization context for the mutation.
103
- // If not, use the query type in order to ensure the client cache could be used.
104
- $ useWrappedType = $ queryNormalizationContext !== $ mutationNormalizationContext ;
105
-
106
- return [
107
- lcfirst ($ resourceMetadata ->getShortName ()) => $ useWrappedType ?
108
- $ this ->getResourceObjectType ($ resourceClass , $ resourceMetadata , $ input , $ queryName , $ mutationName , true , $ depth ) :
109
- $ this ->getResourceObjectType ($ resourceClass , $ resourceMetadata , $ input , $ queryName ?? 'item_query ' , null , true , $ depth ),
110
- 'clientMutationId ' => GraphQLType::string (),
111
- ];
112
- }
113
-
114
- $ fieldsBuilder = $ this ->fieldsBuilderLocator ->get ('api_platform.graphql.fields_builder ' );
115
-
116
- $ fields = $ fieldsBuilder ->getResourceObjectTypeFields ($ resourceClass , $ resourceMetadata , $ input , $ queryName , $ mutationName , $ depth , $ ioMetadata );
117
-
118
- if ($ input && null !== $ mutationName && null !== $ mutationArgs = $ resourceMetadata ->getGraphql ()[$ mutationName ]['args ' ] ?? null ) {
119
- return $ fieldsBuilder ->resolveResourceArgs ($ mutationArgs , $ mutationName , $ resourceMetadata ->getShortName ()) + ['clientMutationId ' => $ fields ['clientMutationId ' ]];
120
- }
121
-
122
- return $ fields ;
123
- },
124
- 'interfaces ' => $ wrapData ? [] : [$ this ->getNodeInterface ()],
125
- ];
126
-
127
- $ resourceObjectType = $ input ? GraphQLType::nonNull (new InputObjectType ($ configuration )) : new ObjectType ($ configuration );
100
+ $ resourceObjectType = $ resourceMetadata ->getInterface ()
101
+ ? $ this ->buildResourceInterfaceType ($ resourceClass , $ shortName , $ resourceMetadata , $ input , $ queryName , $ mutationName , $ wrapped , $ depth )
102
+ : $ this ->buildResourceObjectType ($ resourceClass , $ shortName , $ resourceMetadata , $ input , $ queryName , $ mutationName , $ wrapped , $ depth );
128
103
$ this ->typesContainer ->set ($ shortName , $ resourceObjectType );
129
104
130
105
return $ resourceObjectType ;
@@ -227,4 +202,127 @@ public function isCollection(Type $type): bool
227
202
{
228
203
return $ type ->isCollection () && Type::BUILTIN_TYPE_OBJECT === $ type ->getBuiltinType ();
229
204
}
205
+
206
+ private function buildResourceObjectType (?string $ resourceClass , string $ shortName , ResourceMetadata $ resourceMetadata , bool $ input , ?string $ queryName , ?string $ mutationName , bool $ wrapped , int $ depth )
207
+ {
208
+ $ ioMetadata = $ resourceMetadata ->getGraphqlAttribute ($ mutationName ?? $ queryName , $ input ? 'input ' : 'output ' , null , true );
209
+ if (null !== $ ioMetadata && \array_key_exists ('class ' , $ ioMetadata ) && null !== $ ioMetadata ['class ' ]) {
210
+ $ resourceClass = $ ioMetadata ['class ' ];
211
+ }
212
+
213
+ $ wrapData = !$ wrapped && null !== $ mutationName && !$ input && $ depth < 1 ;
214
+ $ interfaces = ($ interface = $ resourceMetadata ->getImplements ())
215
+ ? $ this ->getInterfaceTypes ($ interface )
216
+ : [];
217
+
218
+ $ configuration = [
219
+ 'name ' => $ shortName ,
220
+ 'description ' => $ resourceMetadata ->getDescription (),
221
+ 'resolveField ' => $ this ->defaultFieldResolver ,
222
+ 'fields ' => function () use ($ resourceClass , $ resourceMetadata , $ input , $ mutationName , $ queryName , $ wrapData , $ depth , $ ioMetadata ) {
223
+ if ($ wrapData ) {
224
+ $ queryNormalizationContext = $ resourceMetadata ->getGraphqlAttribute ($ queryName ?? '' , 'normalization_context ' , [], true );
225
+ $ mutationNormalizationContext = $ resourceMetadata ->getGraphqlAttribute ($ mutationName ?? '' , 'normalization_context ' , [], true );
226
+ // Use a new type for the wrapped object only if there is a specific normalization context for the mutation.
227
+ // If not, use the query type in order to ensure the client cache could be used.
228
+ $ useWrappedType = $ queryNormalizationContext !== $ mutationNormalizationContext ;
229
+
230
+ return [
231
+ lcfirst ($ resourceMetadata ->getShortName ()) => $ useWrappedType ?
232
+ $ this ->getResourceObjectType ($ resourceClass , $ resourceMetadata , $ input , $ queryName , $ mutationName , true , $ depth ) :
233
+ $ this ->getResourceObjectType ($ resourceClass , $ resourceMetadata , $ input , $ queryName ?? 'item_query ' , null , true , $ depth ),
234
+ 'clientMutationId ' => GraphQLType::string (),
235
+ ];
236
+ }
237
+
238
+ $ fieldsBuilder = $ this ->fieldsBuilderLocator ->get ('api_platform.graphql.fields_builder ' );
239
+
240
+ $ fields = $ fieldsBuilder ->getResourceObjectTypeFields ($ resourceClass , $ resourceMetadata , $ input , $ queryName , $ mutationName , $ depth , $ ioMetadata );
241
+
242
+ if ($ input && null !== $ mutationName && null !== $ mutationArgs = $ resourceMetadata ->getGraphql ()[$ mutationName ]['args ' ] ?? null ) {
243
+ return $ fieldsBuilder ->resolveResourceArgs ($ mutationArgs , $ mutationName , $ resourceMetadata ->getShortName ()) + ['clientMutationId ' => $ fields ['clientMutationId ' ]];
244
+ }
245
+
246
+ return $ fields ;
247
+ },
248
+ 'interfaces ' => $ wrapData ? [] : \array_merge ([$ this ->getNodeInterface ()], $ interfaces ),
249
+ ];
250
+
251
+ return $ input ? GraphQLType::nonNull (new InputObjectType ($ configuration )) : new ObjectType ($ configuration );
252
+ }
253
+
254
+ private function buildResourceInterfaceType (?string $ resourceClass , string $ shortName , ResourceMetadata $ resourceMetadata , bool $ input , ?string $ queryName , ?string $ mutationName , bool $ wrapped , int $ depth ): ?InterfaceType
255
+ {
256
+ static $ fieldsBuilder ;
257
+
258
+ $ ioMetadata = $ resourceMetadata ->getGraphqlAttribute ($ mutationName ?? $ queryName , $ input ? 'input ' : 'output ' , null , true );
259
+ if (null !== $ ioMetadata && \array_key_exists ('class ' , $ ioMetadata ) && null !== $ ioMetadata ['class ' ]) {
260
+ $ resourceClass = $ ioMetadata ['class ' ];
261
+ }
262
+
263
+ $ wrapData = !$ wrapped && null !== $ mutationName && !$ input && $ depth < 1 ;
264
+
265
+ if ($ this ->typesContainer ->has ($ shortName )) {
266
+ $ resourceInterface = $ this ->typesContainer ->get ($ shortName );
267
+ if (!$ resourceInterface instanceof InterfaceType) {
268
+ throw new \UnexpectedValueException (sprintf ('Expected GraphQL type "%s" to be %s. ' , $ shortName , InterfaceType::class));
269
+ }
270
+
271
+ return $ resourceInterface ;
272
+ }
273
+
274
+ $ fieldsBuilder = $ fieldsBuilder ?? $ this ->fieldsBuilderLocator ->get ('api_platform.graphql.fields_builder ' );
275
+ $ fields = $ fieldsBuilder ->getResourceObjectTypeFields ($ resourceClass , $ resourceMetadata , false , $ queryName , null , $ depth , null );
276
+
277
+ $ resourceInterface = new InterfaceType ([
278
+ 'name ' => $ shortName ,
279
+ 'description ' => $ resourceMetadata ->getDescription (),
280
+ 'fields ' => $ fields ,
281
+ 'resolveType ' => function ($ value , $ context , $ info ) {
282
+ if (!isset ($ value [ItemNormalizer::ITEM_RESOURCE_CLASS_KEY ])) {
283
+ throw new \UnexpectedValueException ('Resource class was not passed. Interface type can not be used. ' );
284
+ }
285
+
286
+ $ shortName = (new \ReflectionClass ($ value [ItemNormalizer::ITEM_RESOURCE_CLASS_KEY ]))->getShortName ().'Item ' ;
287
+
288
+ if (!$ this ->typesContainer ->has ($ shortName )) {
289
+ throw new \UnexpectedValueException ("Type with name $ shortName can not be found " );
290
+ }
291
+
292
+ $ type = $ this ->typesContainer ->get ($ shortName );
293
+ if (!isset ($ type ->config ['interfaces ' ])) {
294
+ throw new \UnexpectedValueException ("Type \"$ shortName \" doesn't implement any interface. " );
295
+ }
296
+
297
+ foreach ($ type ->config ['interfaces ' ] as $ interface ) {
298
+ if ($ interface === $ info ->returnType ) {
299
+ return $ type ;
300
+ }
301
+ }
302
+
303
+ throw new \UnexpectedValueException ("Type \"$ type \" must implement interface $ info ->returnType " );
304
+ },
305
+ ]);
306
+
307
+ $ this ->typesContainer ->set ($ shortName , $ resourceInterface );
308
+
309
+ return $ resourceInterface ;
310
+ }
311
+
312
+ private function getInterfaceTypes (string $ resourceClass ): array
313
+ {
314
+ try {
315
+ $ reflection = new \ReflectionClass ($ resourceClass );
316
+ } catch (\ReflectionException $ e ) {
317
+ throw new \UnexpectedValueException ("Class $ resourceClass can't be found. " );
318
+ }
319
+
320
+ $ itemTypeName = $ reflection ->getShortName ().self ::INTERFACE_POSTFIX .self ::ITEM_POSTFIX ;
321
+ $ itemType = $ this ->typesContainer ->has ($ itemTypeName ) ? [$ this ->typesContainer ->get ($ itemTypeName )] : [];
322
+
323
+ $ collectionTypeName = $ reflection ->getShortName ().self ::INTERFACE_POSTFIX .self ::COLLECTION_POSTFIX ;
324
+ $ collectionType = $ this ->typesContainer ->has ($ collectionTypeName ) ? [$ this ->typesContainer ->get ($ collectionTypeName )] : [];
325
+
326
+ return \array_merge ($ itemType , $ collectionType );
327
+ }
230
328
}
0 commit comments