From 322c47b9fa3c5913ec9bbeba537a32407151be73 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Tue, 24 Sep 2024 15:56:25 +0200 Subject: [PATCH] feat(2056): support opt out of JMS serializer usage (#2342) | Q | A | |---------------|---------------------------------------------------------------------------------------------------------------------------| | Bug fix? | no | | New feature? | yes | | Deprecations? | no | | Issues | Fix #2056 #2341 | Always registers `JMSModelDescriber` when the bundle is active, independently of the configured global `models.use_jms` option. If `models.use_jms` is set, then `JMSModelDescriber` will be added with a priority of 50, just as before. If `models.use_jms` is not set, it will be added with -50 so that `ObjectModelDescriber` is used instead. This should not lead to any behavior change. Adds support to `options.useJms` in the model by early exiting in either `JMSModelDescriber` or `ObjectModelDescriber` when it is either explicitly `false` or `true`. Together with the priorities of the describer this allows to have either JMS or Symfony serializer as default and switch per model if necessary. In https://github.com/nelmio/NelmioApiDocBundle/issues/2341 I played around with this in an app and it worked as expected. (sorry btw, I'm a fan of rebase + force-push and realized too late that mentioning the issue in the first commit is a bad idea then because GitHub spams whenever I push..) Closes https://github.com/nelmio/NelmioApiDocBundle/issues/2056 Closes https://github.com/nelmio/NelmioApiDocBundle/issues/2341 --------- Co-authored-by: djordy --- CHANGELOG.md | 7 + docs/index.rst | 12 + src/ModelDescriber/JMSModelDescriber.php | 4 + tests/Functional/Configs/JMS.yaml | 3 + .../Controller/JmsOptOutController.php | 41 +++ tests/Functional/ControllerTest.php | 21 +- .../Fixtures/JmsOptOutController.json | 255 ++++++++++++++++++ 7 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 tests/Functional/Configs/JMS.yaml create mode 100644 tests/Functional/Controller/JmsOptOutController.php create mode 100644 tests/Functional/Fixtures/JmsOptOutController.json 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