diff --git a/CHANGELOG.md b/CHANGELOG.md index 1db8677b1..63ee7a3ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 4.31.0 + +* Added support to opt out of JMS serializer usage per endpoint by setting `useJms` in the serializationContext. + ```php + #[OA\Response(response: 200, content: new Model(type: UserDto::class, serializationContext: ["useJms" => false]))] + ``` + ## 4.30.0 * Create top level OpenApi Tag from Tags top level annotations/attributes diff --git a/docs/index.rst b/docs/index.rst index c3c529699..3da4629f4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -509,6 +509,18 @@ General PHP objects nelmio_api_doc: models: { use_jms: false } + Alternatively, it is also possible to opt out of JMS serializer usage per endpoint by setting `useJms` in the serializationContext: + + .. configuration-block:: + + .. code-block:: php-annotations + + /** @OA\Response(response=200, @Model(type=UserDto::class, serializationContext={"useJms"=false})) */ + + .. code-block:: php-attributes + + #[OA\Response(response: 200, content: new Model(type: UserDto::class, serializationContext: ["useJms" => false]))] + When using the JMS serializer combined with `willdurand/Hateoas`_ (and the `BazingaHateoasBundle`_), HATEOAS metadata are automatically extracted diff --git a/src/ModelDescriber/JMSModelDescriber.php b/src/ModelDescriber/JMSModelDescriber.php index a5a9ea66c..46ecdc3c1 100644 --- a/src/ModelDescriber/JMSModelDescriber.php +++ b/src/ModelDescriber/JMSModelDescriber.php @@ -261,6 +261,10 @@ private function computeGroups(Context $context, ?array $type = null): ?array public function supports(Model $model): bool { + if (($model->getSerializationContext()['useJms'] ?? null) === false) { + return false; + } + $className = $model->getType()->getClassName(); try { diff --git a/tests/Functional/Configs/JMS.yaml b/tests/Functional/Configs/JMS.yaml new file mode 100644 index 000000000..37b65232b --- /dev/null +++ b/tests/Functional/Configs/JMS.yaml @@ -0,0 +1,3 @@ +nelmio_api_doc: + models: + use_jms: true diff --git a/tests/Functional/Controller/JmsOptOutController.php b/tests/Functional/Controller/JmsOptOutController.php new file mode 100644 index 000000000..a138e9748 --- /dev/null +++ b/tests/Functional/Controller/JmsOptOutController.php @@ -0,0 +1,41 @@ + false]) + )] + public function jmsOptOut() + { + } +} diff --git a/tests/Functional/ControllerTest.php b/tests/Functional/ControllerTest.php index 0019cf36f..76957178e 100644 --- a/tests/Functional/ControllerTest.php +++ b/tests/Functional/ControllerTest.php @@ -11,8 +11,10 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; +use JMS\SerializerBundle\JMSSerializerBundle; use OpenApi\Annotations as OA; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -42,9 +44,10 @@ protected function getOpenApiDefinition(string $area = 'default'): OA\OpenApi * @dataProvider provideUniversalTestCases * * @param array{name: string, type: string}|null $controller + * @param Bundle[] $extraBundles * @param string[] $extraConfigs */ - public function testControllers(?array $controller, ?string $fixtureName = null, array $extraConfigs = []): void + public function testControllers(?array $controller, ?string $fixtureName = null, array $extraBundles = [], array $extraConfigs = []): void { $controllerName = $controller['name'] ?? null; $controllerType = $controller['type'] ?? null; @@ -59,7 +62,7 @@ public function testControllers(?array $controller, ?string $fixtureName = null, $routes->withPath('/')->import(__DIR__."/Controller/$controllerName.php", $controllerType); }; - $this->configurableContainerFactory->create([], $routingConfiguration, $extraConfigs); + $this->configurableContainerFactory->create($extraBundles, $routingConfiguration, $extraConfigs); $apiDefinition = $this->getOpenApiDefinition(); @@ -88,9 +91,20 @@ public static function provideAttributeTestCases(): \Generator 'type' => $type, ], 'PromotedPropertiesDefaults', + [], [__DIR__.'/Configs/AlternativeNamesPHP81Entities.yaml'], ]; + yield 'JMS model opt out' => [ + [ + 'name' => 'JmsOptOutController', + 'type' => $type, + ], + 'JmsOptOutController', + [new JMSSerializerBundle()], + [__DIR__.'/Configs/JMS.yaml'], + ]; + if (version_compare(Kernel::VERSION, '6.3.0', '>=')) { yield 'https://github.com/nelmio/NelmioApiDocBundle/issues/2209' => [ [ @@ -110,6 +124,7 @@ public static function provideAttributeTestCases(): \Generator 'type' => $type, ], 'MapQueryStringCleanupComponents', + [], [__DIR__.'/Configs/CleanUnusedComponentsProcessor.yaml'], ]; @@ -165,6 +180,7 @@ public static function provideAnnotationTestCases(): \Generator 'type' => 'annotation', ], 'PromotedPropertiesDefaults', + [], [__DIR__.'/Configs/AlternativeNamesPHP80Entities.yaml'], ]; } @@ -178,6 +194,7 @@ public static function provideUniversalTestCases(): \Generator yield 'https://github.com/nelmio/NelmioApiDocBundle/issues/2224' => [ null, 'VendorExtension', + [], [__DIR__.'/Configs/VendorExtension.yaml'], ]; } diff --git a/tests/Functional/Fixtures/JmsOptOutController.json b/tests/Functional/Fixtures/JmsOptOutController.json new file mode 100644 index 000000000..5d376b926 --- /dev/null +++ b/tests/Functional/Fixtures/JmsOptOutController.json @@ -0,0 +1,255 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "", + "version": "0.0.0" + }, + "paths": { + "/api/jms": { + "get": { + "operationId": "get_nelmio_apidoc_tests_functional_jmsoptout_jms", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JMSUser" + } + } + } + } + } + } + }, + "/api/jms_opt_out": { + "get": { + "operationId": "get_nelmio_apidoc_tests_functional_jmsoptout_jmsoptout", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JMSUser2" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "JMSUser": { + "properties": { + "id": { + "title": "userid", + "description": "User id", + "type": "integer", + "default": null, + "readOnly": true, + "example": 1 + }, + "daysOnline": { + "type": "integer", + "default": 0, + "maximum": 300, + "minimum": 1 + }, + "email": { + "type": "string", + "readOnly": false + }, + "roles": { + "title": "roles", + "description": "Roles list", + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "user" + ], + "example": "[\"ADMIN\",\"SUPERUSER\"]" + }, + "location": { + "title": "User Location.", + "type": "string" + }, + "last_update": { + "type": "date" + }, + "friends": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "indexed_friends": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/User" + } + }, + "favorite_dates": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "date-time" + } + }, + "custom_date": { + "type": "string", + "format": "date-time" + }, + "friendsNumber": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "best_friend": { + "$ref": "#/components/schemas/User" + }, + "status": { + "title": "Whether this user is enabled or disabled.", + "description": "Only enabled users may be used in actions.", + "type": "string", + "enum": [ + "disabled", + "enabled" + ] + }, + "virtual_type1": { + "title": "JMS custom types handled via Custom Type Handlers.", + "oneOf": [ + { + "$ref": "#/components/schemas/VirtualTypeClassDoesNotExistsHandlerDefined" + } + ] + }, + "virtual_type2": { + "title": "JMS custom types handled via Custom Type Handlers.", + "oneOf": [ + { + "$ref": "#/components/schemas/VirtualTypeClassDoesNotExistsHandlerNotDefined" + } + ] + }, + "lat_lon_history": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number", + "format": "float" + } + } + }, + "free_form_object": { + "type": "object", + "additionalProperties": true + }, + "free_form_object_without_type": { + "type": "object", + "additionalProperties": true + }, + "deep_object": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "date-time" + } + } + }, + "deep_object_with_items": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "format": "date-time" + } + } + }, + "deep_free_form_object_collection": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "long": { + "type": "string" + }, + "short": { + "type": "integer" + } + }, + "type": "object" + }, + "JMSUser2": { + "required": [ + "dummy" + ], + "properties": { + "roles": { + "title": "roles", + "description": "Roles list", + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "user" + ], + "example": "[\"ADMIN\",\"SUPERUSER\"]" + }, + "dummy": { + "$ref": "#/components/schemas/Dummy" + } + }, + "type": "object" + }, + "User": { + "properties": { + "email": { + "type": "string", + "readOnly": false + }, + "location": { + "title": "User Location.", + "type": "string" + }, + "friends_number": { + "type": "string" + } + }, + "type": "object" + }, + "VirtualTypeClassDoesNotExistsHandlerDefined": {}, + "VirtualTypeClassDoesNotExistsHandlerNotDefined": {}, + "Dummy": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file