From c55d9ef7852fcfe8c1b1263ea33990de6a54de7a Mon Sep 17 00:00:00 2001 From: Djordy Koert Date: Tue, 2 Jan 2024 14:49:24 +0100 Subject: [PATCH] feat: pass serialization context to name converter (#2167) * feat: pass serialization context to name converter * fix: cs * fix: ci php 7.3 failing * fix: ci php 7.3 failing * fix: ci, decorate name converter * fix: ci, decorate name converter * move NameConverter test logic to FunctionTest * fix baseline * remove unnecessary spaces --- Annotation/Model.php | 16 ++++++--- Model/Model.php | 23 ++++++++---- ModelDescriber/ObjectModelDescriber.php | 2 +- OpenApiPhp/ModelRegister.php | 6 ++-- .../Functional/Controller/ApiController80.php | 15 ++++++++ .../Functional/Controller/ApiController81.php | 7 ++++ .../Entity/EntityThroughNameConverter.php | 16 +++++++++ Tests/Functional/FunctionalTest.php | 12 +++++++ .../ModelDescriber/NameConverter.php | 35 +++++++++++++++++++ Tests/Functional/Resources/routes.yaml | 2 +- Tests/Functional/TestKernel.php | 6 ++++ phpunit-baseline.json | 15 ++++++++ 12 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 Tests/Functional/Entity/EntityThroughNameConverter.php create mode 100644 Tests/Functional/ModelDescriber/NameConverter.php diff --git a/Annotation/Model.php b/Annotation/Model.php index 083fbaaa5..b06b4f179 100644 --- a/Annotation/Model.php +++ b/Annotation/Model.php @@ -50,20 +50,28 @@ final class Model extends Attachable public $options; /** - * @param mixed[] $properties - * @param string[] $groups - * @param mixed[] $options + * @var array + */ + public $serializationContext; + + /** + * @param mixed[] $properties + * @param string[] $groups + * @param mixed[] $options + * @param array $serializationContext */ public function __construct( array $properties = [], string $type = Generator::UNDEFINED, array $groups = null, - array $options = null + array $options = null, + array $serializationContext = [] ) { parent::__construct($properties + [ 'type' => $type, 'groups' => $groups, 'options' => $options, + 'serializationContext' => $serializationContext, ]); } } diff --git a/Model/Model.php b/Model/Model.php index a575bfef3..354439373 100644 --- a/Model/Model.php +++ b/Model/Model.php @@ -12,23 +12,26 @@ namespace Nelmio\ApiDocBundle\Model; use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; final class Model { private $type; - private $groups; - private $options; + private $serializationContext; /** * @param string[]|null $groups */ - public function __construct(Type $type, array $groups = null, array $options = null) + public function __construct(Type $type, array $groups = null, array $options = null, array $serializationContext = []) { $this->type = $type; - $this->groups = $groups; $this->options = $options; + $this->serializationContext = $serializationContext; + if (null !== $groups) { + $this->serializationContext[AbstractNormalizer::GROUPS] = $groups; + } } /** @@ -44,12 +47,20 @@ public function getType() */ public function getGroups() { - return $this->groups; + return $this->serializationContext[AbstractNormalizer::GROUPS] ?? null; + } + + /** + * @return array + */ + public function getSerializationContext(): array + { + return $this->serializationContext; } public function getHash(): string { - return md5(serialize([$this->type, $this->groups])); + return md5(serialize([$this->type, $this->getGroups()])); } /** diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 6ae19d345..96d432499 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -117,7 +117,7 @@ public function describe(Model $model, OA\Schema $schema) $propertyInfoProperties = array_intersect($propertyInfoProperties, $this->propertyInfo->getProperties($class, []) ?? []); foreach ($propertyInfoProperties as $propertyName) { - $serializedName = null !== $this->nameConverter ? $this->nameConverter->normalize($propertyName, $class, null, null !== $model->getGroups() ? ['groups' => $model->getGroups()] : []) : $propertyName; + $serializedName = null !== $this->nameConverter ? $this->nameConverter->normalize($propertyName, $class, null, $model->getSerializationContext()) : $propertyName; $reflections = $this->getReflections($reflClass, $propertyName); diff --git a/OpenApiPhp/ModelRegister.php b/OpenApiPhp/ModelRegister.php index a307c5184..c277d6ee1 100644 --- a/OpenApiPhp/ModelRegister.php +++ b/OpenApiPhp/ModelRegister.php @@ -45,7 +45,7 @@ public function __invoke(Analysis $analysis, array $parentGroups = null) if ($annotation instanceof OA\Schema && $annotation->ref instanceof ModelAnnotation) { $model = $annotation->ref; - $annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)); + $annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options, $model->serializationContext)); // It is no longer an unmerged annotation $this->detach($model, $annotation, $analysis); @@ -71,7 +71,7 @@ public function __invoke(Analysis $analysis, array $parentGroups = null) if ($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) { $properties = [ '_context' => Util::createContext(['nested' => $annotation], $annotation->_context), - 'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)), + 'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options, $model->serializationContext)), ]; foreach ($this->mediaTypes as $mediaType) { @@ -98,7 +98,7 @@ public function __invoke(Analysis $analysis, array $parentGroups = null) } $annotation->merge([new $annotationClass([ - 'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)), + 'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options, $model->serializationContext)), ])]); // It is no longer an unmerged annotation diff --git a/Tests/Functional/Controller/ApiController80.php b/Tests/Functional/Controller/ApiController80.php index e41791c58..31688c093 100644 --- a/Tests/Functional/Controller/ApiController80.php +++ b/Tests/Functional/Controller/ApiController80.php @@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArticleInterface; use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityThroughNameConverter; use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithAlternateType80; use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithNullableSchemaSet; use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithObjectType; @@ -468,4 +469,18 @@ public function entityWithNullableSchemaSet() public function serializedNameAction() { } + + /** + * @Route("/name_converter_context", methods={"GET"}) + * + * @OA\Response( + * response="200", + * description="", + * + * @Model(type=EntityThroughNameConverter::class, serializationContext={"secret_name_converter_value"=true}) + * ) + */ + public function nameConverterContext() + { + } } diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index 3b9fc8f94..b99a6a097 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -19,6 +19,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\ArticleInterface; use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity; +use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityThroughNameConverter; use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithAlternateType81; use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithNullableSchemaSet; use Nelmio\ApiDocBundle\Tests\Functional\Entity\EntityWithObjectType; @@ -423,4 +424,10 @@ public function enum() public function serializedNameAction() { } + + #[Route('/name_converter_context', methods: ['GET'])] + #[OA\Response(response: '200', description: '', content: new Model(type: EntityThroughNameConverter::class, serializationContext: ['secret_name_converter_value' => true]))] + public function nameConverterContext() + { + } } diff --git a/Tests/Functional/Entity/EntityThroughNameConverter.php b/Tests/Functional/Entity/EntityThroughNameConverter.php new file mode 100644 index 000000000..baae90202 --- /dev/null +++ b/Tests/Functional/Entity/EntityThroughNameConverter.php @@ -0,0 +1,16 @@ +assertTrue($model->properties[5]->nullable); } + + public function testContextPassedToNameConverter() + { + $this->getOperation('/api/name_converter_context', 'get'); + + $model = $this->getModel('EntityThroughNameConverter'); + $this->assertCount(2, $model->properties); + $this->assertNotHasProperty('id', $model); + $this->assertHasProperty('name_converter_context_id', $model); + $this->assertNotHasProperty('name', $model); + $this->assertHasProperty('name_converter_context_name', $model); + } } diff --git a/Tests/Functional/ModelDescriber/NameConverter.php b/Tests/Functional/ModelDescriber/NameConverter.php new file mode 100644 index 000000000..cd98652e8 --- /dev/null +++ b/Tests/Functional/ModelDescriber/NameConverter.php @@ -0,0 +1,35 @@ +inner = $inner; + } + + public function normalize(string $propertyName, string $class = null, string $format = null, array $context = []): string + { + if (!isset($context['secret_name_converter_value'])) { + return $this->inner->normalize($propertyName, $class, $format, $context); + } + + return 'name_converter_context_'.$propertyName; + } + + public function denormalize(string $propertyName, string $class = null, string $format = null, array $context = []): string + { + throw new \RuntimeException('Was not expected to be called'); + } +} diff --git a/Tests/Functional/Resources/routes.yaml b/Tests/Functional/Resources/routes.yaml index d6cd8b82e..91d24267c 100644 --- a/Tests/Functional/Resources/routes.yaml +++ b/Tests/Functional/Resources/routes.yaml @@ -37,4 +37,4 @@ doc_json: doc_yaml: path: /{area}/docs.yaml - controller: nelmio_api_doc.controller.swagger_yaml \ No newline at end of file + controller: nelmio_api_doc.controller.swagger_yaml diff --git a/Tests/Functional/TestKernel.php b/Tests/Functional/TestKernel.php index cceac67a1..f90cb8298 100644 --- a/Tests/Functional/TestKernel.php +++ b/Tests/Functional/TestKernel.php @@ -24,6 +24,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\NestedGroup\JMSPicture; use Nelmio\ApiDocBundle\Tests\Functional\Entity\PrivateProtectedExposure; use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups; +use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\NameConverter; use Nelmio\ApiDocBundle\Tests\Functional\ModelDescriber\VirtualTypeClassDoesNotExistsHandlerDefinedDescriber; use ReflectionException; use ReflectionMethod; @@ -35,6 +36,7 @@ use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; @@ -349,6 +351,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $def = new Definition(VirtualTypeClassDoesNotExistsHandlerDefinedDescriber::class); $def->addTag('nelmio_api_doc.model_describer'); $c->setDefinition('nelmio.test.jms.virtual_type.describer', $def); + + $c->register('serializer.name_converter.custom', NameConverter::class) + ->setDecoratedService('serializer.name_converter.metadata_aware') + ->addArgument(new Reference('serializer.name_converter.custom.inner')); } public function getCacheDir(): string diff --git a/phpunit-baseline.json b/phpunit-baseline.json index aee840ed2..f999c7fc4 100644 --- a/phpunit-baseline.json +++ b/phpunit-baseline.json @@ -6903,5 +6903,20 @@ "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\ValidationGroupsFunctionalTest::testConstraintDefaultGroupsAreRespectedWhenReadingAnnotations", "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testContextPassedToNameConverter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_class\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testContextPassedToNameConverter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_dir\" service is deprecated since version 5.2", + "count": 1 + }, + { + "location": "Nelmio\\ApiDocBundle\\Tests\\Functional\\FunctionalTest::testContextPassedToNameConverter", + "message": "Since sensio/framework-extra-bundle 5.2: The \"sensio_framework_extra.routing.loader.annot_file\" service is deprecated since version 5.2", + "count": 1 } ]