14
14
namespace ApiPlatform \JsonApi \JsonSchema ;
15
15
16
16
use ApiPlatform \Api \ResourceClassResolverInterface as LegacyResourceClassResolverInterface ;
17
+ use ApiPlatform \JsonSchema \DefinitionNameFactoryInterface ;
18
+ use ApiPlatform \JsonSchema \ResourceMetadataTrait ;
17
19
use ApiPlatform \JsonSchema \Schema ;
18
20
use ApiPlatform \JsonSchema \SchemaFactoryAwareInterface ;
19
21
use ApiPlatform \JsonSchema \SchemaFactoryInterface ;
20
22
use ApiPlatform \Metadata \Operation ;
21
23
use ApiPlatform \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
24
+ use ApiPlatform \Metadata \Resource \Factory \ResourceMetadataCollectionFactoryInterface ;
22
25
use ApiPlatform \Metadata \ResourceClassResolverInterface ;
23
26
24
27
/**
28
31
*/
29
32
final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
30
33
{
34
+ use ResourceMetadataTrait;
31
35
private const LINKS_PROPS = [
32
36
'type ' => 'object ' ,
33
37
'properties ' => [
@@ -102,22 +106,26 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
102
106
],
103
107
];
104
108
105
- public function __construct (private readonly SchemaFactoryInterface $ schemaFactory , private readonly PropertyMetadataFactoryInterface $ propertyMetadataFactory , private readonly ResourceClassResolverInterface |LegacyResourceClassResolverInterface $ resourceClassResolver )
109
+ public function __construct (private readonly SchemaFactoryInterface $ schemaFactory , private readonly PropertyMetadataFactoryInterface $ propertyMetadataFactory , ResourceClassResolverInterface |LegacyResourceClassResolverInterface $ resourceClassResolver, ? ResourceMetadataCollectionFactoryInterface $ resourceMetadataFactory = null , private readonly ? DefinitionNameFactoryInterface $ definitionNameFactory = null )
106
110
{
107
111
if ($ this ->schemaFactory instanceof SchemaFactoryAwareInterface) {
108
112
$ this ->schemaFactory ->setSchemaFactory ($ this );
109
113
}
114
+ $ this ->resourceClassResolver = $ resourceClassResolver ;
115
+ $ this ->resourceMetadataFactory = $ resourceMetadataFactory ;
110
116
}
111
117
112
118
/**
113
119
* {@inheritdoc}
114
120
*/
115
121
public function buildSchema (string $ className , string $ format = 'jsonapi ' , string $ type = Schema::TYPE_OUTPUT , ?Operation $ operation = null , ?Schema $ schema = null , ?array $ serializerContext = null , bool $ forceCollection = false ): Schema
116
122
{
117
- $ schema = $ this ->schemaFactory ->buildSchema ($ className , $ format , $ type , $ operation , $ schema , $ serializerContext , $ forceCollection );
118
123
if ('jsonapi ' !== $ format ) {
119
- return $ schema ;
124
+ return $ this -> schemaFactory -> buildSchema ( $ className , $ format , $ type , $ operation , $ schema, $ serializerContext , $ forceCollection ) ;
120
125
}
126
+ // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources.
127
+ // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes
128
+ $ schema = $ this ->schemaFactory ->buildSchema ($ className , $ format , $ type , $ operation , $ schema , [], $ forceCollection );
121
129
122
130
if (($ key = $ schema ->getRootDefinitionKey ()) || ($ key = $ schema ->getItemsDefinitionKey ())) {
123
131
$ definitions = $ schema ->getDefinitions ();
@@ -128,7 +136,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
128
136
return $ schema ;
129
137
}
130
138
131
- $ definitions [$ key ]['properties ' ] = $ this ->buildDefinitionPropertiesSchema ($ key , $ className , $ schema , $ serializerContext );
139
+ $ definitions [$ key ]['properties ' ] = $ this ->buildDefinitionPropertiesSchema ($ key , $ className , $ format , $ type , $ operation , $ schema , [] );
132
140
133
141
if ($ schema ->getRootDefinitionKey ()) {
134
142
return $ schema ;
@@ -166,17 +174,27 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
166
174
}
167
175
}
168
176
169
- private function buildDefinitionPropertiesSchema (string $ key , string $ className , Schema $ schema , ?array $ serializerContext ): array
177
+ private function buildDefinitionPropertiesSchema (string $ key , string $ className , string $ format , string $ type , ? Operation $ operation , Schema $ schema , ?array $ serializerContext ): array
170
178
{
171
179
$ definitions = $ schema ->getDefinitions ();
172
180
$ properties = $ definitions [$ key ]['properties ' ] ?? [];
173
181
174
182
$ attributes = [];
175
183
$ relationships = [];
184
+ $ relatedDefinitions = [];
176
185
foreach ($ properties as $ propertyName => $ property ) {
177
186
if ($ relation = $ this ->getRelationship ($ className , $ propertyName , $ serializerContext )) {
178
- [$ isOne , $ isMany ] = $ relation ;
187
+ [$ isOne , $ hasOperations , $ relatedClassName ] = $ relation ;
188
+ if (false === $ hasOperations ) {
189
+ continue ;
190
+ }
179
191
192
+ $ operation = $ this ->findOperation ($ relatedClassName , $ type , $ operation , $ serializerContext );
193
+ $ inputOrOutputClass = $ this ->findOutputClass ($ relatedClassName , $ type , $ operation , $ serializerContext );
194
+ $ serializerContext ??= $ this ->getSerializerContext ($ operation , $ type );
195
+ $ definitionName = $ this ->definitionNameFactory ->create ($ relatedClassName , $ format , $ inputOrOutputClass , $ operation , $ serializerContext );
196
+ $ ref = Schema::VERSION_OPENAPI === $ schema ->getVersion () ? '#/components/schemas/ ' .$ definitionName : '#/definitions/ ' .$ definitionName ;
197
+ $ relatedDefinitions [$ propertyName ] = ['$ref ' => $ ref ];
180
198
if ($ isOne ) {
181
199
$ relationships [$ propertyName ]['properties ' ]['data ' ] = self ::RELATION_PROPS ;
182
200
continue ;
@@ -197,11 +215,25 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
197
215
$ replacement = self ::PROPERTY_PROPS ;
198
216
$ replacement ['attributes ' ]['properties ' ] = $ attributes ;
199
217
218
+ $ included = [];
200
219
if (\count ($ relationships ) > 0 ) {
201
220
$ replacement ['relationships ' ] = [
202
221
'type ' => 'object ' ,
203
222
'properties ' => $ relationships ,
204
223
];
224
+ $ included = [
225
+ 'included ' => [
226
+ 'description ' => 'Related resources requested via the "include" query parameter. ' ,
227
+ 'type ' => 'array ' ,
228
+ 'items ' => [
229
+ 'anyOf ' => array_values ($ relatedDefinitions ),
230
+ ],
231
+ 'readOnly ' => true ,
232
+ 'externalDocs ' => [
233
+ 'url ' => 'https://jsonapi.org/format/#fetching-includes ' ,
234
+ ],
235
+ ],
236
+ ];
205
237
}
206
238
207
239
if ($ required = $ definitions [$ key ]['required ' ] ?? null ) {
@@ -223,7 +255,7 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
223
255
'properties ' => $ replacement ,
224
256
'required ' => ['type ' , 'id ' ],
225
257
],
226
- ];
258
+ ] + $ included ;
227
259
}
228
260
229
261
private function getRelationship (string $ resourceClass , string $ property , ?array $ serializerContext ): ?array
@@ -232,6 +264,7 @@ private function getRelationship(string $resourceClass, string $property, ?array
232
264
$ types = $ propertyMetadata ->getBuiltinTypes () ?? [];
233
265
$ isRelationship = false ;
234
266
$ isOne = $ isMany = false ;
267
+ $ className = $ hasOperations = null ;
235
268
236
269
foreach ($ types as $ type ) {
237
270
if ($ type ->isCollection ()) {
@@ -244,8 +277,13 @@ private function getRelationship(string $resourceClass, string $property, ?array
244
277
continue ;
245
278
}
246
279
$ isRelationship = true ;
280
+ $ resourceMetadata = $ this ->resourceMetadataFactory ->create ($ className );
281
+ $ operation = $ resourceMetadata ->getOperation ();
282
+ // @see https://github.com/api-platform/core/issues/5501
283
+ // @see https://github.com/api-platform/core/pull/5722
284
+ $ hasOperations ??= $ operation ->canRead ();
247
285
}
248
286
249
- return $ isRelationship ? [$ isOne , $ isMany ] : null ;
287
+ return $ isRelationship ? [$ isOne , $ hasOperations , $ className ] : null ;
250
288
}
251
289
}
0 commit comments