1414namespace ApiPlatform \JsonApi \JsonSchema ;
1515
1616use ApiPlatform \Api \ResourceClassResolverInterface as LegacyResourceClassResolverInterface ;
17+ use ApiPlatform \JsonSchema \DefinitionNameFactoryInterface ;
18+ use ApiPlatform \JsonSchema \ResourceMetadataTrait ;
1719use ApiPlatform \JsonSchema \Schema ;
1820use ApiPlatform \JsonSchema \SchemaFactoryAwareInterface ;
1921use ApiPlatform \JsonSchema \SchemaFactoryInterface ;
2022use ApiPlatform \Metadata \Operation ;
2123use ApiPlatform \Metadata \Property \Factory \PropertyMetadataFactoryInterface ;
24+ use ApiPlatform \Metadata \Resource \Factory \ResourceMetadataCollectionFactoryInterface ;
2225use ApiPlatform \Metadata \ResourceClassResolverInterface ;
2326
2427/**
2831 */
2932final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface
3033{
34+ use ResourceMetadataTrait;
3135 private const LINKS_PROPS = [
3236 'type ' => 'object ' ,
3337 'properties ' => [
@@ -102,22 +106,26 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI
102106 ],
103107 ];
104108
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 )
106110 {
107111 if ($ this ->schemaFactory instanceof SchemaFactoryAwareInterface) {
108112 $ this ->schemaFactory ->setSchemaFactory ($ this );
109113 }
114+ $ this ->resourceClassResolver = $ resourceClassResolver ;
115+ $ this ->resourceMetadataFactory = $ resourceMetadataFactory ;
110116 }
111117
112118 /**
113119 * {@inheritdoc}
114120 */
115121 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
116122 {
117- $ schema = $ this ->schemaFactory ->buildSchema ($ className , $ format , $ type , $ operation , $ schema , $ serializerContext , $ forceCollection );
118123 if ('jsonapi ' !== $ format ) {
119- return $ schema ;
124+ return $ this -> schemaFactory -> buildSchema ( $ className , $ format , $ type , $ operation , $ schema, $ serializerContext , $ forceCollection ) ;
120125 }
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 );
121129
122130 if (($ key = $ schema ->getRootDefinitionKey ()) || ($ key = $ schema ->getItemsDefinitionKey ())) {
123131 $ definitions = $ schema ->getDefinitions ();
@@ -128,7 +136,7 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin
128136 return $ schema ;
129137 }
130138
131- $ definitions [$ key ]['properties ' ] = $ this ->buildDefinitionPropertiesSchema ($ key , $ className , $ schema , $ serializerContext );
139+ $ definitions [$ key ]['properties ' ] = $ this ->buildDefinitionPropertiesSchema ($ key , $ className , $ format , $ type , $ operation , $ schema , [] );
132140
133141 if ($ schema ->getRootDefinitionKey ()) {
134142 return $ schema ;
@@ -166,17 +174,27 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
166174 }
167175 }
168176
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
170178 {
171179 $ definitions = $ schema ->getDefinitions ();
172180 $ properties = $ definitions [$ key ]['properties ' ] ?? [];
173181
174182 $ attributes = [];
175183 $ relationships = [];
184+ $ relatedDefinitions = [];
176185 foreach ($ properties as $ propertyName => $ property ) {
177186 if ($ relation = $ this ->getRelationship ($ className , $ propertyName , $ serializerContext )) {
178- [$ isOne , $ isMany ] = $ relation ;
187+ [$ isOne , $ hasOperations , $ relatedClassName ] = $ relation ;
188+ if (false === $ hasOperations ) {
189+ continue ;
190+ }
179191
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 ];
180198 if ($ isOne ) {
181199 $ relationships [$ propertyName ]['properties ' ]['data ' ] = self ::RELATION_PROPS ;
182200 continue ;
@@ -197,11 +215,25 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
197215 $ replacement = self ::PROPERTY_PROPS ;
198216 $ replacement ['attributes ' ]['properties ' ] = $ attributes ;
199217
218+ $ included = [];
200219 if (\count ($ relationships ) > 0 ) {
201220 $ replacement ['relationships ' ] = [
202221 'type ' => 'object ' ,
203222 'properties ' => $ relationships ,
204223 ];
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+ ];
205237 }
206238
207239 if ($ required = $ definitions [$ key ]['required ' ] ?? null ) {
@@ -223,7 +255,7 @@ private function buildDefinitionPropertiesSchema(string $key, string $className,
223255 'properties ' => $ replacement ,
224256 'required ' => ['type ' , 'id ' ],
225257 ],
226- ];
258+ ] + $ included ;
227259 }
228260
229261 private function getRelationship (string $ resourceClass , string $ property , ?array $ serializerContext ): ?array
@@ -232,6 +264,7 @@ private function getRelationship(string $resourceClass, string $property, ?array
232264 $ types = $ propertyMetadata ->getBuiltinTypes () ?? [];
233265 $ isRelationship = false ;
234266 $ isOne = $ isMany = false ;
267+ $ className = $ hasOperations = null ;
235268
236269 foreach ($ types as $ type ) {
237270 if ($ type ->isCollection ()) {
@@ -244,8 +277,13 @@ private function getRelationship(string $resourceClass, string $property, ?array
244277 continue ;
245278 }
246279 $ 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 ();
247285 }
248286
249- return $ isRelationship ? [$ isOne , $ isMany ] : null ;
287+ return $ isRelationship ? [$ isOne , $ hasOperations , $ className ] : null ;
250288 }
251289}
0 commit comments