From 16be828f41b6858a7775576f443e5a528cef547f Mon Sep 17 00:00:00 2001 From: Faizan Akram Date: Mon, 11 Dec 2023 10:32:46 +0100 Subject: [PATCH] feat: symfony 7 support - makes doctrine/annotations optional - updates tests and ci to test with both symfony 7 and < symfony 7 version - adds routes-attributes.yaml file for symfony 7 compatible controllers/routes - skips tests/packages related to packages like jms serializer, bazinga, etc not supported by symfony 7 for symfony 7 matrix --- .github/workflows/continuous-integration.yml | 15 +- Command/DumpCommand.php | 5 +- .../Compiler/ConfigurationPass.php | 3 +- DependencyInjection/NelmioApiDocExtension.php | 7 +- Describer/OpenApiPhpDescriber.php | 25 +- .../Annotations/AnnotationsReader.php | 8 +- .../Annotations/OpenApiAnnotationsReader.php | 19 +- .../SymfonyConstraintAnnotationReader.php | 14 +- ModelDescriber/FormModelDescriber.php | 7 +- ModelDescriber/JMSModelDescriber.php | 5 +- ModelDescriber/ObjectModelDescriber.php | 4 +- Resources/config/fos_rest.xml | 2 +- Resources/config/services.xml | 2 +- RouteDescriber/FosRestDescriber.php | 8 +- Routing/FilteredRouteCollectionBuilder.php | 11 +- Tests/Functional/BazingaFunctionalTest.php | 5 + Tests/Functional/Controller/ApiController.php | 7 +- .../Functional/Controller/ApiController81.php | 2 +- .../Controller/ClassApiController.php | 35 +- .../Controller/InvokableController.php | 42 +- .../Controller/SerializedNameController.php | 48 +- .../Functional/Controller/TestController.php | 38 +- .../Controller/UndocumentedController.php | 37 +- Tests/Functional/Entity/Article.php | 34 +- .../Entity/EntityWithAlternateType.php | 46 +- .../Entity/EntityWithNullableSchemaSet.php | 129 +++-- .../Entity/EntityWithObjectType.php | 28 +- Tests/Functional/Entity/EntityWithRef.php | 27 +- .../Functional/Entity/SymfonyConstraints.php | 534 ++++++++++++------ ...SymfonyConstraintsWithValidationGroups.php | 79 ++- .../Entity/SymfonyDiscriminator.php | 35 +- Tests/Functional/Entity/User.php | 388 +++++++++---- .../Symfony7/SerializedNameEntity.php | 34 ++ Tests/Functional/FOSRestTest.php | 5 + Tests/Functional/FunctionalTest.php | 6 +- .../Resources/routes-attributes.yaml | 40 ++ Tests/Functional/TestKernel.php | 191 ++++--- Tests/Functional/WebTestCase.php | 2 +- .../Annotations/AnnotationReaderTest.php | 6 +- .../Fixture/CompoundValidationRule.php | 1 + .../SymfonyConstraintAnnotationReaderTest.php | 40 +- Tests/RouteDescriber/FosRestDescriberTest.php | 16 +- .../FilteredRouteCollectionBuilderTest.php | 48 +- composer.json | 43 +- 44 files changed, 1409 insertions(+), 672 deletions(-) create mode 100644 Tests/Functional/EntityExcluded/Symfony7/SerializedNameEntity.php create mode 100644 Tests/Functional/Resources/routes-attributes.yaml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ac6c48f14..785e3bd40 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -34,6 +34,8 @@ jobs: symfony-require: "5.4.*" - php-version: 8.1 symfony-require: "6.3.*" + - php-version: 8.2 + symfony-require: "7.0.*" steps: - name: "Checkout" @@ -59,7 +61,7 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - - name: "Install dependencies with composer" + - name: "Prepare dependencies with composer" env: SYMFONY_REQUIRE: "${{ matrix.symfony-require }}" run: | @@ -67,5 +69,16 @@ jobs: composer global require --no-progress --no-scripts --no-plugins symfony/flex composer update --no-interaction --no-progress ${{ matrix.composer-flags }} + - name: Remove packages not compatible symfony 7 + if: matrix.symfony-require == '7.0' + run: | + composer remove friendsofsymfony/rest-bundle sensio/framework-extra-bundle jms/serializer-bundle willdurand/hateoas-bundle --no-update --dev + + - name: "Install dependencies with composer" + env: + SYMFONY_REQUIRE: "${{ matrix.symfony-require }}" + run: | + composer update --no-interaction --no-progress ${{ matrix.composer-flags }} + - name: PHPUnit Tests run: vendor/bin/simple-phpunit --configuration phpunit.xml.dist --coverage-text diff --git a/Command/DumpCommand.php b/Command/DumpCommand.php index 3fa9d7ab4..29c78968f 100644 --- a/Command/DumpCommand.php +++ b/Command/DumpCommand.php @@ -62,10 +62,7 @@ protected function configure(): void ; } - /** - * @return int|void - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $area = $input->getOption('area'); $format = $input->getOption('format'); diff --git a/DependencyInjection/Compiler/ConfigurationPass.php b/DependencyInjection/Compiler/ConfigurationPass.php index c2400fb44..df91bddc1 100644 --- a/DependencyInjection/Compiler/ConfigurationPass.php +++ b/DependencyInjection/Compiler/ConfigurationPass.php @@ -14,6 +14,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\FormModelDescriber; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; /** @@ -29,7 +30,7 @@ public function process(ContainerBuilder $container): void $container->register('nelmio_api_doc.model_describers.form', FormModelDescriber::class) ->setPublic(false) ->addArgument(new Reference('form.factory')) - ->addArgument(new Reference('annotations.reader')) + ->addArgument(new Reference('annotations.reader', ContainerInterface::NULL_ON_INVALID_REFERENCE)) ->addArgument($container->getParameter('nelmio_api_doc.media_types')) ->addArgument($container->getParameter('nelmio_api_doc.use_validation_groups')) ->addTag('nelmio_api_doc.model_describer', ['priority' => 100]); diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index cf6ac501e..edcca5cb7 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -25,6 +25,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -94,7 +95,7 @@ public function load(array $configs, ContainerBuilder $container): void ->setArguments([ new Reference(sprintf('nelmio_api_doc.routes.%s', $area)), new Reference('nelmio_api_doc.controller_reflector'), - new Reference('annotations.reader'), // We cannot use the cached version of the annotation reader since the construction of the annotations is context dependant... + new Reference('annotations.reader', ContainerInterface::NULL_ON_INVALID_REFERENCE), // We cannot use the cached version of the annotation reader since the construction of the annotations is context dependant... new Reference('logger'), ]) ->addTag(sprintf('nelmio_api_doc.describer.%s', $area), ['priority' => -200]); @@ -123,7 +124,7 @@ public function load(array $configs, ContainerBuilder $container): void (new Definition(FilteredRouteCollectionBuilder::class)) ->setArguments( [ - new Reference('annotation_reader'), // Here we use the cached version as we don't deal with @OA annotations in this service + new Reference('annotation_reader', ContainerInterface::NULL_ON_INVALID_REFERENCE), // Here we use the cached version as we don't deal with @OA annotations in this service new Reference('nelmio_api_doc.controller_reflector'), $area, $areaConfig, @@ -181,7 +182,7 @@ public function load(array $configs, ContainerBuilder $container): void ->setPublic(false) ->setArguments([ new Reference('jms_serializer.metadata_factory'), - new Reference('annotations.reader'), + new Reference('annotations.reader', ContainerInterface::NULL_ON_INVALID_REFERENCE), $config['media_types'], $jmsNamingStrategy, $container->getParameter('nelmio_api_doc.use_validation_groups'), diff --git a/Describer/OpenApiPhpDescriber.php b/Describer/OpenApiPhpDescriber.php index f40e59c95..62156a611 100644 --- a/Describer/OpenApiPhpDescriber.php +++ b/Describer/OpenApiPhpDescriber.php @@ -33,11 +33,15 @@ final class OpenApiPhpDescriber private $routeCollection; private $controllerReflector; + + /** + * @var Reader|null + */ private $annotationReader; private $logger; private $overwrite; - public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, bool $overwrite = false) + public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, ?Reader $annotationReader, LoggerInterface $logger, bool $overwrite = false) { $this->routeCollection = $routeCollection; $this->controllerReflector = $controllerReflector; @@ -51,7 +55,7 @@ public function describe(OA\OpenApi $api) $classAnnotations = []; /** @var \ReflectionMethod $method */ - foreach ($this->getMethodsToParse() as $method => list($path, $httpMethods, $routeName)) { + foreach ($this->getMethodsToParse() as $method => [$path, $httpMethods, $routeName]) { $declaringClass = $method->getDeclaringClass(); $path = Util::getPath($api, $path); @@ -65,16 +69,25 @@ public function describe(OA\OpenApi $api) $this->setContext($context); if (!array_key_exists($declaringClass->getName(), $classAnnotations)) { - $classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) { + if (null !== $this->annotationReader) { + $classAnnotations = $this->annotationReader->getClassAnnotations($declaringClass); + } + + $classAnnotations = array_filter($classAnnotations, function ($v) { return $v instanceof OA\AbstractAnnotation; }); + $classAnnotations = array_merge($classAnnotations, $this->getAttributesAsAnnotation($declaringClass, $context)); $classAnnotations[$declaringClass->getName()] = $classAnnotations; } - $annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) { - return $v instanceof OA\AbstractAnnotation; - }); + $annotations = []; + if (null !== $this->annotationReader) { + $annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) { + return $v instanceof OA\AbstractAnnotation; + }); + } + $annotations = array_merge($annotations, $this->getAttributesAsAnnotation($method, $context)); if (0 === count($annotations) && 0 === count($classAnnotations[$declaringClass->getName()])) { diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php index 85f496dcf..b483793ce 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -21,22 +21,16 @@ */ class AnnotationsReader { - private $annotationsReader; - private $modelRegistry; - private $phpDocReader; private $openApiAnnotationsReader; private $symfonyConstraintAnnotationReader; public function __construct( - Reader $annotationsReader, + ?Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes, bool $useValidationGroups = false ) { - $this->annotationsReader = $annotationsReader; - $this->modelRegistry = $modelRegistry; - $this->phpDocReader = new PropertyPhpDocReader(); $this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes); $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader( diff --git a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php index e0f1753ea..026178d2a 100644 --- a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php +++ b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php @@ -28,10 +28,13 @@ class OpenApiAnnotationsReader { use SetsContextTrait; + /** + * @var Reader|null + */ private $annotationsReader; private $modelRegister; - public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes) + public function __construct(?Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes) { $this->annotationsReader = $annotationsReader; $this->modelRegister = new ModelRegister($modelRegistry, $mediaTypes); @@ -97,12 +100,14 @@ private function getAnnotation(Context $parentContext, $reflection, string $clas } } - if ($reflection instanceof \ReflectionClass) { - return $this->annotationsReader->getClassAnnotation($reflection, $className); - } elseif ($reflection instanceof \ReflectionProperty) { - return $this->annotationsReader->getPropertyAnnotation($reflection, $className); - } elseif ($reflection instanceof \ReflectionMethod) { - return $this->annotationsReader->getMethodAnnotation($reflection, $className); + if (null !== $this->annotationsReader) { + if ($reflection instanceof \ReflectionClass) { + return $this->annotationsReader->getClassAnnotation($reflection, $className); + } elseif ($reflection instanceof \ReflectionProperty) { + return $this->annotationsReader->getPropertyAnnotation($reflection, $className); + } elseif ($reflection instanceof \ReflectionMethod) { + return $this->annotationsReader->getMethodAnnotation($reflection, $className); + } } } finally { $this->setContext(null); diff --git a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php index aa5701849..ee3b9bb6d 100644 --- a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php +++ b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php @@ -28,7 +28,7 @@ class SymfonyConstraintAnnotationReader use SetsContextTrait; /** - * @var Reader + * @var Reader|null */ private $annotationsReader; @@ -42,7 +42,7 @@ class SymfonyConstraintAnnotationReader */ private $useValidationGroups; - public function __construct(Reader $annotationsReader, bool $useValidationGroups=false) + public function __construct(?Reader $annotationsReader, bool $useValidationGroups=false) { $this->annotationsReader = $annotationsReader; $this->useValidationGroups = $useValidationGroups; @@ -215,10 +215,12 @@ private function locateAnnotations($reflection): \Traversable } } - if ($reflection instanceof \ReflectionProperty) { - yield from $this->annotationsReader->getPropertyAnnotations($reflection); - } elseif ($reflection instanceof \ReflectionMethod) { - yield from $this->annotationsReader->getMethodAnnotations($reflection); + if (null !== $this->annotationsReader) { + if ($reflection instanceof \ReflectionProperty) { + yield from $this->annotationsReader->getPropertyAnnotations($reflection); + } elseif ($reflection instanceof \ReflectionMethod) { + yield from $this->annotationsReader->getMethodAnnotations($reflection); + } } } diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 933d411f4..4a5fd70a2 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -40,6 +40,10 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry use SetsContextTrait; private $formFactory; + + /** + * @var Reader|null + */ private $doctrineReader; private $mediaTypes; private $useValidationGroups; @@ -52,9 +56,6 @@ public function __construct( ) { $this->formFactory = $formFactory; $this->doctrineReader = $reader; - if (null === $reader) { - @trigger_error(sprintf('Not passing a doctrine reader to the constructor of %s is deprecated since version 3.8 and won\'t be allowed in version 5.', self::class), E_USER_DEPRECATED); - } if (null === $mediaTypes) { $mediaTypes = ['json']; diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index 991228fd2..6ab63be04 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -40,6 +40,9 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn private $namingStrategy; + /** + * @var Reader|null + */ private $doctrineReader; private $contexts = []; @@ -60,7 +63,7 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn public function __construct( MetadataFactoryInterface $factory, - Reader $reader, + ?Reader $reader, array $mediaTypes, ?PropertyNamingStrategyInterface $namingStrategy = null, bool $useValidationGroups = false, diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 6f81f7b97..6ae19d345 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -35,7 +35,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar private $propertyInfo; /** @var ClassMetadataFactoryInterface|null */ private $classMetadataFactory; - /** @var Reader */ + /** @var Reader|null */ private $doctrineReader; /** @var PropertyDescriberInterface[] */ private $propertyDescribers; @@ -48,7 +48,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar public function __construct( PropertyInfoExtractorInterface $propertyInfo, - Reader $reader, + ?Reader $reader, iterable $propertyDescribers, array $mediaTypes, NameConverterInterface $nameConverter = null, diff --git a/Resources/config/fos_rest.xml b/Resources/config/fos_rest.xml index 32c3045a2..74c99ba82 100644 --- a/Resources/config/fos_rest.xml +++ b/Resources/config/fos_rest.xml @@ -5,7 +5,7 @@ - + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 0d9801312..9ccd5bad4 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -75,7 +75,7 @@ - + diff --git a/RouteDescriber/FosRestDescriber.php b/RouteDescriber/FosRestDescriber.php index e4c4e1a93..e7bbeeaa9 100644 --- a/RouteDescriber/FosRestDescriber.php +++ b/RouteDescriber/FosRestDescriber.php @@ -27,13 +27,13 @@ final class FosRestDescriber implements RouteDescriberInterface { use RouteDescriberTrait; - /** @var Reader */ + /** @var Reader|null */ private $annotationReader; /** @var string[] */ private $mediaTypes; - public function __construct(Reader $annotationReader, array $mediaTypes) + public function __construct(?Reader $annotationReader, array $mediaTypes) { $this->annotationReader = $annotationReader; $this->mediaTypes = $mediaTypes; @@ -41,7 +41,9 @@ public function __construct(Reader $annotationReader, array $mediaTypes) public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod) { - $annotations = $this->annotationReader->getMethodAnnotations($reflectionMethod); + $annotations = null !== $this->annotationReader + ? $this->annotationReader->getMethodAnnotations($reflectionMethod) + : []; $annotations = array_filter($annotations, static function ($value) { return $value instanceof RequestParam || $value instanceof QueryParam; }); diff --git a/Routing/FilteredRouteCollectionBuilder.php b/Routing/FilteredRouteCollectionBuilder.php index 518603f69..f1fcf5313 100644 --- a/Routing/FilteredRouteCollectionBuilder.php +++ b/Routing/FilteredRouteCollectionBuilder.php @@ -21,7 +21,7 @@ final class FilteredRouteCollectionBuilder { - /** @var Reader */ + /** @var Reader|null */ private $annotationReader; /** @var ControllerReflector */ @@ -34,7 +34,7 @@ final class FilteredRouteCollectionBuilder private $options; public function __construct( - Reader $annotationReader, + ?Reader $annotationReader, ControllerReflector $controllerReflector, string $area, array $options = [] @@ -137,7 +137,7 @@ private function matchAnnotation(Route $route): bool /** @var Areas|null $areas */ $areas = $this->getAttributesAsAnnotation($reflectionMethod->getDeclaringClass(), Areas::class)[0] ?? null; - if (null === $areas) { + if (null === $areas && null !== $this->annotationReader) { /** @var Areas|null $areas */ $areas = $this->annotationReader->getMethodAnnotation( $reflectionMethod, @@ -167,7 +167,10 @@ private function defaultRouteDisabled(Route $route): bool return false; } - $annotations = $this->annotationReader->getMethodAnnotations($method); + $annotations = null !== $this->annotationReader + ? $this->annotationReader->getMethodAnnotations($method) + : []; + if (method_exists(\ReflectionMethod::class, 'getAttributes')) { $annotations = array_merge($annotations, array_map(function (\ReflectionAttribute $attribute) { return $attribute->newInstance(); diff --git a/Tests/Functional/BazingaFunctionalTest.php b/Tests/Functional/BazingaFunctionalTest.php index d7f914609..e10514f34 100644 --- a/Tests/Functional/BazingaFunctionalTest.php +++ b/Tests/Functional/BazingaFunctionalTest.php @@ -12,12 +12,17 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use Hateoas\Configuration\Embedded; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; class BazingaFunctionalTest extends WebTestCase { protected function setUp(): void { + if (Kernel::MAJOR_VERSION < 7) { + $this->markTestSkipped('Not supported in symfony 7'); + } + parent::setUp(); static::createClient([], ['HTTP_HOST' => 'api.example.com']); diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 8985a5098..0dbf4efb3 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -11,12 +11,11 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; -if (\PHP_VERSION_ID >= 80100) { - /** - * @Route("/api", name="api_", host="api.example.com") - */ +if (Kernel::MAJOR_VERSION >= 7) { + #[Route("/api", name: 'api_', host: 'api.example.com')] class ApiController extends ApiController81 { } diff --git a/Tests/Functional/Controller/ApiController81.php b/Tests/Functional/Controller/ApiController81.php index b12f5cd06..69d8ed490 100644 --- a/Tests/Functional/Controller/ApiController81.php +++ b/Tests/Functional/Controller/ApiController81.php @@ -32,7 +32,7 @@ class ApiController81 extends ApiController80 ])] #[OA\Parameter(ref: '#/components/parameters/test')] #[Route('/article_attributes/{id}', methods: ['GET'])] - #[OA\Parameter(name: 'Accept-Version', in: 'header', attachables: [new OA\Schema(type: 'string')])] + #[OA\Parameter(name: 'Accept-Version', in: 'header', schema: new OA\Schema(type: 'string'))] public function fetchArticleActionWithAttributes() { } diff --git a/Tests/Functional/Controller/ClassApiController.php b/Tests/Functional/Controller/ClassApiController.php index 21bbd0e45..a3a4b6d19 100644 --- a/Tests/Functional/Controller/ClassApiController.php +++ b/Tests/Functional/Controller/ClassApiController.php @@ -13,19 +13,34 @@ use Nelmio\ApiDocBundle\Annotation\Security; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Response; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route("/api", host="api.example.com") - * @Security(name="basic") - */ -class ClassApiController -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @Route("/security/class") - * @OA\Response(response="201", description="") + * @Route("/api", host="api.example.com") + * @Security(name="basic") */ - public function securityAction() + class ClassApiController + { + /** + * @Route("/security/class") + * @OA\Response(response="201", description="") + */ + public function securityAction() + { + } + } +} else { + #[Security(name: 'basic')] + #[Route("/api", host: "api.example.com")] + class ClassApiController { + #[Response(response: 201, description: '')] + #[Route("/security/class")] + public function securityAction() + { + } } -} +} \ No newline at end of file diff --git a/Tests/Functional/Controller/InvokableController.php b/Tests/Functional/Controller/InvokableController.php index b640b70be..86e4a9dbb 100644 --- a/Tests/Functional/Controller/InvokableController.php +++ b/Tests/Functional/Controller/InvokableController.php @@ -12,20 +12,36 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Response; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; -/** - * Prevents a regression (see https://github.com/nelmio/NelmioApiDocBundle/issues/1559). - * - * @Route("/api/invoke", host="api.example.com", name="invokable", methods={"GET"}) - * @OA\Response( - * response=200, - * description="Invokable!" - * ) - */ -class InvokableController -{ - public function __invoke() +if (Kernel::MAJOR_VERSION < 7) { + /** + * Prevents a regression (see https://github.com/nelmio/NelmioApiDocBundle/issues/1559). + * + * @Route("/api/invoke", host="api.example.com", name="invokable", methods={"GET"}) + * @OA\Response( + * response=200, + * description="Invokable!" + * ) + */ + class InvokableController + { + public function __invoke() + { + } + } +} else { + /** + * Prevents a regression (see https://github.com/nelmio/NelmioApiDocBundle/issues/1559). + */ + #[Response(response: 200, description: 'Invokable!')] + #[Route("/api/invoke", host: "api.example.com", name: "invokable", methods: ["GET"])] + class InvokableController { + public function __invoke() + { + } } -} +} \ No newline at end of file diff --git a/Tests/Functional/Controller/SerializedNameController.php b/Tests/Functional/Controller/SerializedNameController.php index 736a2669f..dd9f3fa7b 100644 --- a/Tests/Functional/Controller/SerializedNameController.php +++ b/Tests/Functional/Controller/SerializedNameController.php @@ -13,25 +13,43 @@ use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\SerializedNameEnt; +use Nelmio\ApiDocBundle\Tests\Functional\EntityExcluded\Symfony7\SerializedNameEntity; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Response; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; -/** - * This controller is only loaded when SerializedName exists (sf >= 4.2). - * - * @Route("/api", host="api.example.com") - */ -class SerializedNameController -{ +if (Kernel::MAJOR_VERSION < 7) { + /** + * This controller is only loaded when SerializedName exists (sf >= 4.2). + * + * @Route("/api", host="api.example.com") + */ + class SerializedNameController + { + /** + * @OA\Response( + * response="200", + * description="success", + * @Model(type=SerializedNameEnt::class) + * ) + * @Route("/serializename", methods={"GET"}) + */ + public function serializedNameAction() + { + } + } +} else { /** - * @OA\Response( - * response="200", - * description="success", - * @Model(type=SerializedNameEnt::class) - * ) - * @Route("/serializename", methods={"GET"}) + * This controller is only loaded when SerializedName exists (sf >= 4.2). */ - public function serializedNameAction() + #[Route("/api", host: "api.example.com")] + class SerializedNameController { + #[Response(response: 200, description: 'success', attachables: [new Model(type: SerializedNameEntity::class)])] + #[Route("/serializename", methods: ["GET"])] + public function serializedNameAction() + { + } } -} +} \ No newline at end of file diff --git a/Tests/Functional/Controller/TestController.php b/Tests/Functional/Controller/TestController.php index 7e887e613..71bd8b1e4 100644 --- a/Tests/Functional/Controller/TestController.php +++ b/Tests/Functional/Controller/TestController.php @@ -12,21 +12,35 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Response; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route("/test", host="api-test.example.com") - */ -class TestController -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @OA\Response( - * response="200", - * description="Test" - * ) - * @Route("/test/", methods={"GET"}) + * @Route("/test", host="api-test.example.com") */ - public function testAction() + class TestController + { + /** + * @OA\Response( + * response="200", + * description="Test" + * ) + * @Route("/test/", methods={"GET"}) + */ + public function testAction() + { + } + } +} else { + #[Route('/test', host: 'api-test.example.com')] + class TestController { + #[Response(response: 200, description: 'Test')] + #[Route("/test/", methods: ["GET"])] + public function testAction() + { + } } -} +} \ No newline at end of file diff --git a/Tests/Functional/Controller/UndocumentedController.php b/Tests/Functional/Controller/UndocumentedController.php index 98fbe6562..92904cb1e 100644 --- a/Tests/Functional/Controller/UndocumentedController.php +++ b/Tests/Functional/Controller/UndocumentedController.php @@ -11,19 +11,36 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Controller; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route(host="api.example.com") - */ -class UndocumentedController -{ + +if (Kernel::MAJOR_VERSION < 7) { /** - * This path is excluded by the config (only /api allowed). - * - * @Route("/undocumented", methods={"GET"}) + * @Route(host="api.example.com") */ - public function undocumentedAction() + class UndocumentedController + { + /** + * This path is excluded by the config (only /api allowed). + * + * @Route("/undocumented", methods={"GET"}) + */ + public function undocumentedAction() + { + } + } +} else { + #[Route(host: "api.example.com")] + class UndocumentedController { + /** + * This path is excluded by the config (only /api allowed). + * + */ + #[Route("/undocumented", methods: ["GET"])] + public function undocumentedAction() + { + } } -} +} \ No newline at end of file diff --git a/Tests/Functional/Entity/Article.php b/Tests/Functional/Entity/Article.php index 922e54620..a67505b01 100644 --- a/Tests/Functional/Entity/Article.php +++ b/Tests/Functional/Entity/Article.php @@ -11,21 +11,37 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; +use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Serializer\Annotation\Groups; -/** - * @author Guilhem N. - */ -class Article -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @Groups({"light"}) + * @author Guilhem N. */ - public function setAuthor(User $author) + class Article { - } + /** + * @Groups({"light"}) + */ + public function setAuthor(User $author) + { + } - public function setContent(string $content) + public function setContent(string $content) + { + } + } +} else { + class Article { + #[Groups(["light"])] + public function setAuthor(User $author) + { + } + + public function setContent(string $content) + { + } } } diff --git a/Tests/Functional/Entity/EntityWithAlternateType.php b/Tests/Functional/Entity/EntityWithAlternateType.php index 1b9538c05..9f5c03afd 100644 --- a/Tests/Functional/Entity/EntityWithAlternateType.php +++ b/Tests/Functional/Entity/EntityWithAlternateType.php @@ -14,22 +14,44 @@ use ArrayIterator; use IteratorAggregate; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Items; +use OpenApi\Attributes\Schema; +use Symfony\Component\HttpKernel\Kernel; -/** - * @OA\Schema(type="array", @OA\Items(type="string")) - */ -class EntityWithAlternateType implements IteratorAggregate -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @var string + * @OA\Schema(type="array", @OA\Items(type="string")) */ - public $ignored = 'this property should be ignored because of the annotation above'; + class EntityWithAlternateType implements IteratorAggregate + { + /** + * @var string + */ + public $ignored = 'this property should be ignored because of the annotation above'; - public function getIterator(): ArrayIterator + public function getIterator(): ArrayIterator + { + return new ArrayIterator([ + 'abc', + 'def', + ]); + } + } +} else { + #[Schema(type: 'array', items: new Items(type: 'string'))] + class EntityWithAlternateType implements IteratorAggregate { - return new ArrayIterator([ - 'abc', - 'def', - ]); + /** + * @var string + */ + public $ignored = 'this property should be ignored because of the annotation above'; + + public function getIterator(): ArrayIterator + { + return new ArrayIterator([ + 'abc', + 'def', + ]); + } } } diff --git a/Tests/Functional/Entity/EntityWithNullableSchemaSet.php b/Tests/Functional/Entity/EntityWithNullableSchemaSet.php index 615a41f0e..216800936 100644 --- a/Tests/Functional/Entity/EntityWithNullableSchemaSet.php +++ b/Tests/Functional/Entity/EntityWithNullableSchemaSet.php @@ -12,48 +12,91 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Property; +use Symfony\Component\HttpKernel\Kernel; -class EntityWithNullableSchemaSet -{ - /** - * @var ?string - * - * @OA\Property() - */ - public $nullablePropertyNullableNotSet; - - /** - * @var ?string - * - * @OA\Property(nullable=false) - */ - public $nullablePropertyNullableFalseSet; - - /** - * @var ?string - * - * @OA\Property(nullable=true) - */ - public $nullablePropertyNullableTrueSet; - - /** - * @var string - * - * @OA\Property() - */ - public $nonNullablePropertyNullableNotSet; - - /** - * @var string - * - * @OA\Property(nullable=false) - */ - public $nonNullablePropertyNullableFalseSet; - - /** - * @var string - * - * @OA\Property(nullable=true) - */ - public $nonNullablePropertyNullableTrueSet; +if (Kernel::MAJOR_VERSION < 7) { + class EntityWithNullableSchemaSet + { + /** + * @var ?string + * + * @OA\Property() + */ + public $nullablePropertyNullableNotSet; + + /** + * @var ?string + * + * @OA\Property(nullable=false) + */ + public $nullablePropertyNullableFalseSet; + + /** + * @var ?string + * + * @OA\Property(nullable=true) + */ + public $nullablePropertyNullableTrueSet; + + /** + * @var string + * + * @OA\Property() + */ + public $nonNullablePropertyNullableNotSet; + + /** + * @var string + * + * @OA\Property(nullable=false) + */ + public $nonNullablePropertyNullableFalseSet; + + /** + * @var string + * + * @OA\Property(nullable=true) + */ + public $nonNullablePropertyNullableTrueSet; + } +} else { + class EntityWithNullableSchemaSet + { + /** + * @var ?string + */ + #[Property] + public $nullablePropertyNullableNotSet; + + /** + * @var ?string + */ + #[Property(nullable: false)] + public $nullablePropertyNullableFalseSet; + + /** + * @var ?string + */ + #[Property(nullable: true)] + public $nullablePropertyNullableTrueSet; + + /** + * @var string + */ + #[Property] + public $nonNullablePropertyNullableNotSet; + + /** + * @var string + */ + #[Property(nullable: false)] + public $nonNullablePropertyNullableFalseSet; + + /** + * @var string + */ + #[Property(nullable: true)] + public $nonNullablePropertyNullableTrueSet; + } } diff --git a/Tests/Functional/Entity/EntityWithObjectType.php b/Tests/Functional/Entity/EntityWithObjectType.php index ba0415c23..1d17f8cdb 100644 --- a/Tests/Functional/Entity/EntityWithObjectType.php +++ b/Tests/Functional/Entity/EntityWithObjectType.php @@ -12,14 +12,28 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Schema; +use Symfony\Component\HttpKernel\Kernel; -/** - * @OA\Schema(type="object") - */ -class EntityWithObjectType -{ + +if (Kernel::MAJOR_VERSION < 7) { /** - * @var string + * @OA\Schema(type="object") */ - public $notIgnored = 'this should be read'; + class EntityWithObjectType + { + /** + * @var string + */ + public $notIgnored = 'this should be read'; + } +} else { + #[Schema(type: 'object')] + class EntityWithObjectType + { + /** + * @var string + */ + public $notIgnored = 'this should be read'; + } } diff --git a/Tests/Functional/Entity/EntityWithRef.php b/Tests/Functional/Entity/EntityWithRef.php index f149bfb29..ca48276c2 100644 --- a/Tests/Functional/Entity/EntityWithRef.php +++ b/Tests/Functional/Entity/EntityWithRef.php @@ -12,14 +12,27 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Schema; +use Symfony\Component\HttpKernel\Kernel; -/** - * @OA\Schema(ref="#/components/schemas/Test") - */ -class EntityWithRef -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @var string + * @OA\Schema(ref="#/components/schemas/Test") */ - public $ignored = 'this property should be ignored because of the annotation above'; + class EntityWithRef + { + /** + * @var string + */ + public $ignored = 'this property should be ignored because of the annotation above'; + } +} else { + #[Schema(ref: '#/components/schemas/Test')] + class EntityWithRef + { + /** + * @var string + */ + public $ignored = 'this property should be ignored because of the annotation above'; + } } diff --git a/Tests/Functional/Entity/SymfonyConstraints.php b/Tests/Functional/Entity/SymfonyConstraints.php index 86e3d1600..b802b460b 100644 --- a/Tests/Functional/Entity/SymfonyConstraints.php +++ b/Tests/Functional/Entity/SymfonyConstraints.php @@ -12,189 +12,365 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Validator\Constraints as Assert; -class SymfonyConstraints -{ - /** - * @var int - * - * @Assert\NotBlank(groups={"test"}) - */ - private $propertyNotBlank; - - /** - * @var int - * - * @Assert\NotNull() - */ - private $propertyNotNull; - - /** - * @var int - * - * @Assert\Length(min="0", max="50") - */ - private $propertyAssertLength; - - /** - * @var int - * - * @Assert\Regex(pattern="/[a-z]{2}/") - */ - private $propertyRegex; - - /** - * @var int - * - * @Assert\Count(min="0", max="10") - */ - private $propertyCount; - - /** - * @var int - * - * @Assert\Choice(choices={"choice1", "choice2"}) - */ - private $propertyChoice; - - /** - * @var int - * - * @Assert\Choice(callback={SymfonyConstraints::class,"fetchAllowedChoices"}) - */ - private $propertyChoiceWithCallback; - - /** - * @var int - * - * @Assert\Choice(callback="fetchAllowedChoices") - */ - private $propertyChoiceWithCallbackWithoutClass; - - /** - * @var string[] - * - * @Assert\Choice(multiple=true, choices={"choice1", "choice2"}) - */ - private $propertyChoiceWithMultiple; - - /** - * @var int - * - * @Assert\Expression( - * "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()", - * message="If this is a tech post, the category should be either php or symfony!" - * ) - */ - private $propertyExpression; - - /** - * @var int - * - * @Assert\Range(min=1, max=5) - */ - private $propertyRange; - - /** - * @var int - * - * @Assert\LessThan(42) - */ - private $propertyLessThan; - - /** - * @var int - * - * @Assert\LessThanOrEqual(23) - */ - private $propertyLessThanOrEqual; - - /** - * @var int - * - * @CustomAssert\CompoundValidationRule() - */ - private $propertyWithCompoundValidationRule; - - public function setPropertyWithCompoundValidationRule(int $propertyWithCompoundValidationRule): void +if (Kernel::MAJOR_VERSION < 7) { + class SymfonyConstraints { - $this->propertyWithCompoundValidationRule = $propertyWithCompoundValidationRule; + /** + * @var int + * + * @Assert\NotBlank(groups={"test"}) + */ + private $propertyNotBlank; + + /** + * @var int + * + * @Assert\NotNull() + */ + private $propertyNotNull; + + /** + * @var int + * + * @Assert\Length(min="0", max="50") + */ + private $propertyAssertLength; + + /** + * @var int + * + * @Assert\Regex(pattern="/[a-z]{2}/") + */ + private $propertyRegex; + + /** + * @var int + * + * @Assert\Count(min="0", max="10") + */ + private $propertyCount; + + /** + * @var int + * + * @Assert\Choice(choices={"choice1", "choice2"}) + */ + private $propertyChoice; + + /** + * @var int + * + * @Assert\Choice(callback={SymfonyConstraints::class,"fetchAllowedChoices"}) + */ + private $propertyChoiceWithCallback; + + /** + * @var int + * + * @Assert\Choice(callback="fetchAllowedChoices") + */ + private $propertyChoiceWithCallbackWithoutClass; + + /** + * @var string[] + * + * @Assert\Choice(multiple=true, choices={"choice1", "choice2"}) + */ + private $propertyChoiceWithMultiple; + + /** + * @var int + * + * @Assert\Expression( + * "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()", + * message="If this is a tech post, the category should be either php or symfony!" + * ) + */ + private $propertyExpression; + + /** + * @var int + * + * @Assert\Range(min=1, max=5) + */ + private $propertyRange; + + /** + * @var int + * + * @Assert\LessThan(42) + */ + private $propertyLessThan; + + /** + * @var int + * + * @Assert\LessThanOrEqual(23) + */ + private $propertyLessThanOrEqual; + + /** + * @var int + * + * @CustomAssert\CompoundValidationRule() + */ + private $propertyWithCompoundValidationRule; + + public function setPropertyWithCompoundValidationRule(int $propertyWithCompoundValidationRule): void + { + $this->propertyWithCompoundValidationRule = $propertyWithCompoundValidationRule; + } + + /** + * @Assert\Count(min="0", max="10") + */ + public function setPropertyNotBlank(int $propertyNotBlank): void + { + $this->propertyNotBlank = $propertyNotBlank; + } + + public function setPropertyNotNull(int $propertyNotNull): void + { + $this->propertyNotNull = $propertyNotNull; + } + + public function setPropertyAssertLength(int $propertyAssertLength): void + { + $this->propertyAssertLength = $propertyAssertLength; + } + + public function setPropertyRegex(int $propertyRegex): void + { + $this->propertyRegex = $propertyRegex; + } + + public function setPropertyCount(int $propertyCount): void + { + $this->propertyCount = $propertyCount; + } + + public function setPropertyChoice(int $propertyChoice): void + { + $this->propertyChoice = $propertyChoice; + } + + public function setPropertyChoiceWithCallback(int $propertyChoiceWithCallback): void + { + $this->propertyChoiceWithCallback = $propertyChoiceWithCallback; + } + + public function setPropertyChoiceWithCallbackWithoutClass(int $propertyChoiceWithCallbackWithoutClass): void + { + $this->propertyChoiceWithCallbackWithoutClass = $propertyChoiceWithCallbackWithoutClass; + } + + public function setPropertyChoiceWithMultiple(array $propertyChoiceWithMultiple): void + { + $this->propertyChoiceWithMultiple = $propertyChoiceWithMultiple; + } + + public function setPropertyExpression(int $propertyExpression): void + { + $this->propertyExpression = $propertyExpression; + } + + public function setPropertyRange(int $propertyRange): void + { + $this->propertyRange = $propertyRange; + } + + public function setPropertyLessThan(int $propertyLessThan): void + { + $this->propertyLessThan = $propertyLessThan; + } + + public function setPropertyLessThanOrEqual(int $propertyLessThanOrEqual): void + { + $this->propertyLessThanOrEqual = $propertyLessThanOrEqual; + } + + /** + * @return array + */ + public static function fetchAllowedChoices() + { + return ['choice1', 'choice2']; + } } - - /** - * @Assert\Count(min="0", max="10") - */ - public function setPropertyNotBlank(int $propertyNotBlank): void - { - $this->propertyNotBlank = $propertyNotBlank; - } - - public function setPropertyNotNull(int $propertyNotNull): void - { - $this->propertyNotNull = $propertyNotNull; - } - - public function setPropertyAssertLength(int $propertyAssertLength): void - { - $this->propertyAssertLength = $propertyAssertLength; - } - - public function setPropertyRegex(int $propertyRegex): void - { - $this->propertyRegex = $propertyRegex; - } - - public function setPropertyCount(int $propertyCount): void - { - $this->propertyCount = $propertyCount; - } - - public function setPropertyChoice(int $propertyChoice): void - { - $this->propertyChoice = $propertyChoice; - } - - public function setPropertyChoiceWithCallback(int $propertyChoiceWithCallback): void - { - $this->propertyChoiceWithCallback = $propertyChoiceWithCallback; - } - - public function setPropertyChoiceWithCallbackWithoutClass(int $propertyChoiceWithCallbackWithoutClass): void - { - $this->propertyChoiceWithCallbackWithoutClass = $propertyChoiceWithCallbackWithoutClass; - } - - public function setPropertyChoiceWithMultiple(array $propertyChoiceWithMultiple): void - { - $this->propertyChoiceWithMultiple = $propertyChoiceWithMultiple; - } - - public function setPropertyExpression(int $propertyExpression): void - { - $this->propertyExpression = $propertyExpression; - } - - public function setPropertyRange(int $propertyRange): void - { - $this->propertyRange = $propertyRange; - } - - public function setPropertyLessThan(int $propertyLessThan): void - { - $this->propertyLessThan = $propertyLessThan; - } - - public function setPropertyLessThanOrEqual(int $propertyLessThanOrEqual): void - { - $this->propertyLessThanOrEqual = $propertyLessThanOrEqual; - } - - /** - * @return array - */ - public static function fetchAllowedChoices() +} else { + class SymfonyConstraints { - return ['choice1', 'choice2']; + /** + * @var int + */ + #[Assert\NotBlank(groups: ["test"])] + private $propertyNotBlank; + + /** + * @var int + * + */ + #[Assert\NotNull()] + private $propertyNotNull; + + /** + * @var int + * + */ + #[Assert\Length(min: "0", max: "50")] + private $propertyAssertLength; + + /** + * @var int + * + */ + #[Assert\Regex(pattern: "/[a-z]{2}/")] + private $propertyRegex; + + /** + * @var int + * + */ + #[Assert\Count(min: "0", max: "10")] + private $propertyCount; + + /** + * @var int + */ + #[Assert\Choice(choices: ["choice1", "choice2"])] + private $propertyChoice; + + /** + * @var int + */ + #[Assert\Choice(callback: [self::class, "fetchAllowedChoices"])] + private $propertyChoiceWithCallback; + + /** + * @var int + */ + #[Assert\Choice(callback: "fetchAllowedChoices")] + private $propertyChoiceWithCallbackWithoutClass; + + /** + * @var string[] + */ + #[Assert\Choice(multiple: true, choices: ["choice1", "choice2"])] + private $propertyChoiceWithMultiple; + + /** + * @var int + */ + #[Assert\Expression( + "this.getCategory() in ['php', 'symfony'] or !this.isTechnicalPost()", + message: "If this is a tech post, the category should be either php or symfony!" + )] + private $propertyExpression; + + /** + * @var int + */ + #[Assert\Range(min: 1, max: 5)] + private $propertyRange; + + /** + * @var int + */ + #[Assert\LessThan(42)] + private $propertyLessThan; + + /** + * @var int + */ + #[Assert\LessThanOrEqual(23)] + private $propertyLessThanOrEqual; + + /** + * @var int + */ + #[CustomAssert\CompoundValidationRule] + private $propertyWithCompoundValidationRule; + + public function setPropertyWithCompoundValidationRule(int $propertyWithCompoundValidationRule): void + { + $this->propertyWithCompoundValidationRule = $propertyWithCompoundValidationRule; + } + + #[Assert\Count(min: "0", max: "10")] + public function setPropertyNotBlank(int $propertyNotBlank): void + { + $this->propertyNotBlank = $propertyNotBlank; + } + + public function setPropertyNotNull(int $propertyNotNull): void + { + $this->propertyNotNull = $propertyNotNull; + } + + public function setPropertyAssertLength(int $propertyAssertLength): void + { + $this->propertyAssertLength = $propertyAssertLength; + } + + public function setPropertyRegex(int $propertyRegex): void + { + $this->propertyRegex = $propertyRegex; + } + + public function setPropertyCount(int $propertyCount): void + { + $this->propertyCount = $propertyCount; + } + + public function setPropertyChoice(int $propertyChoice): void + { + $this->propertyChoice = $propertyChoice; + } + + public function setPropertyChoiceWithCallback(int $propertyChoiceWithCallback): void + { + $this->propertyChoiceWithCallback = $propertyChoiceWithCallback; + } + + public function setPropertyChoiceWithCallbackWithoutClass(int $propertyChoiceWithCallbackWithoutClass): void + { + $this->propertyChoiceWithCallbackWithoutClass = $propertyChoiceWithCallbackWithoutClass; + } + + public function setPropertyChoiceWithMultiple(array $propertyChoiceWithMultiple): void + { + $this->propertyChoiceWithMultiple = $propertyChoiceWithMultiple; + } + + public function setPropertyExpression(int $propertyExpression): void + { + $this->propertyExpression = $propertyExpression; + } + + public function setPropertyRange(int $propertyRange): void + { + $this->propertyRange = $propertyRange; + } + + public function setPropertyLessThan(int $propertyLessThan): void + { + $this->propertyLessThan = $propertyLessThan; + } + + public function setPropertyLessThanOrEqual(int $propertyLessThanOrEqual): void + { + $this->propertyLessThanOrEqual = $propertyLessThanOrEqual; + } + + /** + * @return array + */ + public static function fetchAllowedChoices() + { + return ['choice1', 'choice2']; + } } } diff --git a/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php b/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php index 8a986dd75..cf83b6cb3 100644 --- a/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php +++ b/Tests/Functional/Entity/SymfonyConstraintsWithValidationGroups.php @@ -12,32 +12,61 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Items; +use OpenApi\Attributes\Property; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; -class SymfonyConstraintsWithValidationGroups -{ - /** - * @var int - * - * @Groups("test") - * @Assert\NotBlank(groups={"test"}) - * @Assert\Range(min=1, max=100) - */ - public $property; - - /** - * @var int - * - * @Assert\Range(min=1, max=100) - */ - public $propertyInDefaultGroup; - - /** - * @var array - * - * @OA\Property(type="array", @OA\Items(type="string")) - * @Assert\Valid - */ - public $propertyArray; +if (Kernel::MAJOR_VERSION < 7) { + class SymfonyConstraintsWithValidationGroups + { + /** + * @var int + * + * @Groups("test") + * @Assert\NotBlank(groups={"test"}) + * @Assert\Range(min=1, max=100) + */ + public $property; + + /** + * @var int + * + * @Assert\Range(min=1, max=100) + */ + public $propertyInDefaultGroup; + + /** + * @var array + * + * @OA\Property(type="array", @OA\Items(type="string")) + * @Assert\Valid + */ + public $propertyArray; + } +} else { + class SymfonyConstraintsWithValidationGroups + { + /** + * @var int + */ + #[Assert\Range(min: 1, max: 100)] + #[Assert\NotBlank(groups: ["test"])] + #[Groups("test")] + public $property; + + /** + * @var int + */ + #[Assert\Range(min: 1, max: 100)] + public $propertyInDefaultGroup; + + /** + * @var array + */ + #[Property(type: 'array', items: new Items(type: 'string'))] + #[Assert\Valid] + public $propertyArray; + } } diff --git a/Tests/Functional/Entity/SymfonyDiscriminator.php b/Tests/Functional/Entity/SymfonyDiscriminator.php index cd4e562ad..4341b6159 100644 --- a/Tests/Functional/Entity/SymfonyDiscriminator.php +++ b/Tests/Functional/Entity/SymfonyDiscriminator.php @@ -11,18 +11,33 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; -/** - * @DiscriminatorMap(typeProperty="type", mapping={ - * "one": SymfonyDiscriminatorOne::class, - * "two": SymfonyDiscriminatorTwo::class, - * }) - */ -abstract class SymfonyDiscriminator -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @var string + * @DiscriminatorMap(typeProperty="type", mapping={ + * "one": SymfonyDiscriminatorOne::class, + * "two": SymfonyDiscriminatorTwo::class, + * }) */ - public $type; + abstract class SymfonyDiscriminator + { + /** + * @var string + */ + public $type; + } +} else { + #[DiscriminatorMap( + typeProperty: 'type', + mapping: ['one' => SymfonyDiscriminatorOne::class, 'two' => SymfonyDiscriminatorTwo::class] + )] + abstract class SymfonyDiscriminator + { + /** + * @var string + */ + public $type; + } } diff --git a/Tests/Functional/Entity/User.php b/Tests/Functional/Entity/User.php index 041838c43..c9e9b42dc 100644 --- a/Tests/Functional/Entity/User.php +++ b/Tests/Functional/Entity/User.php @@ -12,157 +12,301 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Entity; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Property; +use Symfony\Component\HttpKernel\Kernel; -/** - * @author Guilhem N. - */ -class User -{ +if (Kernel::MAJOR_VERSION < 7) { /** - * @var int - * - * @OA\Property(description = "User id", readOnly = true, title = "userid", default = null) + * @author Guilhem N. */ - private $id; + class User + { + /** + * @var int + * + * @OA\Property(description = "User id", readOnly = true, title = "userid", default = null) + */ + private $id; - /** - * @OA\Property(type="string", readOnly = false) - */ - private $email; + /** + * @OA\Property(type="string", readOnly = false) + */ + private $email; - /** - * User Roles Comment. - * - * @var string[] - * - * @OA\Property( - * description = "User roles", - * title = "roles", - * example="[""ADMIN"",""SUPERUSER""]", - * default = {"user"}, - * ) - */ - private $roles; + /** + * User Roles Comment. + * + * @var string[] + * + * @OA\Property( + * description = "User roles", + * title = "roles", + * example="[""ADMIN"",""SUPERUSER""]", + * default = {"user"}, + * ) + */ + private $roles; - /** - * User Location. - * - * @OA\Property(type = "string") - */ - private $location; + /** + * User Location. + * + * @OA\Property(type = "string") + */ + private $location; - /** - * @var int - * - * @OA\Property(type = "string") - */ - private $friendsNumber; + /** + * @var int + * + * @OA\Property(type = "string") + */ + private $friendsNumber; - /** - * @var float - * @OA\Property(default = 0.0) - */ - private $money; + /** + * @var float + * @OA\Property(default = 0.0) + */ + private $money; - /** - * @var \DateTime - * @OA\Property(property="creationDate") - */ - private $createdAt; + /** + * @var \DateTime + * @OA\Property(property="creationDate") + */ + private $createdAt; - /** - * @var User[] - */ - private $users; + /** + * @var User[] + */ + private $users; - /** - * @var User|null - */ - private $friend; + /** + * @var User|null + */ + private $friend; - /** - * @var User[]|null - */ - private $friends; + /** + * @var User[]|null + */ + private $friends; - /** - * @var string - * - * @OA\Property(enum = {"disabled", "enabled"}) - */ - private $status; + /** + * @var string + * + * @OA\Property(enum = {"disabled", "enabled"}) + */ + private $status; - /** - * @var \DateTimeInterface - */ - private $dateAsInterface; + /** + * @var \DateTimeInterface + */ + private $dateAsInterface; - public function setMoney(float $money) - { - $this->money = $money; - } + public function setMoney(float $money) + { + $this->money = $money; + } - /** - * @OA\Property(example=1) - */ - public function setId(int $id) - { - $this->id = $id; - } + /** + * @OA\Property(example=1) + */ + public function setId(int $id) + { + $this->id = $id; + } - public function setEmail($email) - { - $this->email = $email; - } + public function setEmail($email) + { + $this->email = $email; + } + + /** + * @param string[] $roles + */ + public function setRoles(array $roles) + { + $this->roles = $roles; + } + + public function setLocation(string $location) + { + } + + public function setFriendsNumber(int $friendsNumber) + { + $this->friendsNumber = $friendsNumber; + } + + public function setCreatedAt(\DateTime $createAt) + { + } + + public function setUsers(array $users) + { + } + + public function setFriend(self $friend = null) + { + } + + public function setFriends(array $friends = []) + { + } + public function setDummy(Dummy $dummy) + { + } + + public function setStatus(string $status) + { + } + + public function getDateAsInterface(): \DateTimeInterface + { + return $this->dateAsInterface; + } + + public function setDateAsInterface(\DateTimeInterface $dateAsInterface) + { + $this->dateAsInterface = $dateAsInterface; + } + } +} else { /** - * @param string[] $roles + * @author Guilhem N. */ - public function setRoles(array $roles) + class User { - $this->roles = $roles; - } + /** + * @var int + */ + #[Property(description: 'User id', readOnly: true, title: 'userid', default: null)] + private $id; - public function setLocation(string $location) - { - } + #[Property(type: 'string', readOnly: false)] + private $email; - public function setFriendsNumber(int $friendsNumber) - { - $this->friendsNumber = $friendsNumber; - } + /** + * User Roles Comment. + * + * @var string[] + */ + #[Property(description: 'User roles', title: 'roles', example: ['ADMIN', 'SUPERUSER'], default: ['USER'])] + private $roles; - public function setCreatedAt(\DateTime $createAt) - { - } + /** + * User Location. + */ + #[Property(type: 'string')] + private $location; - public function setUsers(array $users) - { - } + /** + * @var int + */ + #[Property(type: 'string')] + private $friendsNumber; - public function setFriend(self $friend = null) - { - } + /** + * @var float + */ + #[Property(default: 0.0)] + private $money; - public function setFriends(array $friends = []) - { - } + /** + * @var \DateTime + */ + #[Property(property: 'creationDate')] + private $createdAt; - public function setDummy(Dummy $dummy) - { - } + /** + * @var User[] + */ + private $users; - public function setStatus(string $status) - { - } + /** + * @var User|null + */ + private $friend; - public function getDateAsInterface(): \DateTimeInterface - { - return $this->dateAsInterface; - } + /** + * @var User[]|null + */ + private $friends; - public function setDateAsInterface(\DateTimeInterface $dateAsInterface) - { - $this->dateAsInterface = $dateAsInterface; + /** + * @var string + */ + #[Property(enum: ['disabled', 'enabled'])] + private $status; + + /** + * @var \DateTimeInterface + */ + private $dateAsInterface; + + public function setMoney(float $money) + { + $this->money = $money; + } + + #[Property(example: 1)] + public function setId(int $id) + { + $this->id = $id; + } + + public function setEmail($email) + { + $this->email = $email; + } + + /** + * @param string[] $roles + */ + public function setRoles(array $roles) + { + $this->roles = $roles; + } + + public function setLocation(string $location) + { + } + + public function setFriendsNumber(int $friendsNumber) + { + $this->friendsNumber = $friendsNumber; + } + + public function setCreatedAt(\DateTime $createAt) + { + } + + public function setUsers(array $users) + { + } + + public function setFriend(self $friend = null) + { + } + + public function setFriends(array $friends = []) + { + } + + public function setDummy(Dummy $dummy) + { + } + + public function setStatus(string $status) + { + } + + public function getDateAsInterface(): \DateTimeInterface + { + return $this->dateAsInterface; + } + + public function setDateAsInterface(\DateTimeInterface $dateAsInterface) + { + $this->dateAsInterface = $dateAsInterface; + } } -} +} \ No newline at end of file diff --git a/Tests/Functional/EntityExcluded/Symfony7/SerializedNameEntity.php b/Tests/Functional/EntityExcluded/Symfony7/SerializedNameEntity.php new file mode 100644 index 000000000..6cf7d4f16 --- /dev/null +++ b/Tests/Functional/EntityExcluded/Symfony7/SerializedNameEntity.php @@ -0,0 +1,34 @@ + + */ +class SerializedNameEntity +{ + /** + * @var string + */ + #[SerializedName("notfoo")] + public $foo; + + /** + * Tests serialized name feature. + */ + #[SerializedName("notwhatyouthink")] + public function setBar(string $bar) + { + } +} diff --git a/Tests/Functional/FOSRestTest.php b/Tests/Functional/FOSRestTest.php index b500512ec..561c21108 100644 --- a/Tests/Functional/FOSRestTest.php +++ b/Tests/Functional/FOSRestTest.php @@ -13,11 +13,16 @@ use OpenApi\Annotations as OA; use OpenApi\Generator; +use Symfony\Component\HttpKernel\Kernel; class FOSRestTest extends WebTestCase { protected function setUp(): void { + if (Kernel::MAJOR_VERSION < 7) { + $this->markTestSkipped('Not supported in symfony 7'); + } + parent::setUp(); static::createClient([], ['HTTP_HOST' => 'api.example.com']); diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 7c9bb784d..7a3498b66 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -11,11 +11,13 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; +use Doctrine\Common\Annotations\Reader; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\Tests\Helper; use OpenApi\Annotations as OAAnnotations; use OpenApi\Attributes as OAAttributes; use OpenApi\Generator; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Serializer\Annotation\SerializedName; class FunctionalTest extends WebTestCase @@ -62,7 +64,9 @@ public function testFetchArticleAction(string $articleRoute) public function provideArticleRoute(): iterable { - yield 'Annotations' => ['/api/article/{id}']; + if (interface_exists(Reader::class) && Kernel::MAJOR_VERSION < 7) { + yield 'Annotations' => ['/api/article/{id}']; + } if (\PHP_VERSION_ID >= 80100) { yield 'Attributes' => ['/api/article_attributes/{id}']; diff --git a/Tests/Functional/Resources/routes-attributes.yaml b/Tests/Functional/Resources/routes-attributes.yaml new file mode 100644 index 000000000..eab7d3a37 --- /dev/null +++ b/Tests/Functional/Resources/routes-attributes.yaml @@ -0,0 +1,40 @@ +# Resources +test: + resource: ../Controller/TestController.php + type: attribute + +api: + resource: ../Controller/ApiController.php + type: attribute + +class_api: + resource: ../Controller/ClassApiController.php + type: attribute + +undocumented: + resource: ../Controller/UndocumentedController.php + type: attribute + +invokable: + resource: ../Controller/InvokableController.php + type: attribute + +api_platform: + resource: . + prefix: /api + type: api_platform + +# Controllers +doc_area: + path: /{area}/docs + controller: nelmio_api_doc.controller.swagger_ui + defaults: + area: default + +doc_json: + path: /{area}/docs.json + controller: nelmio_api_doc.controller.swagger_json + +doc_yaml: + path: /{area}/docs.yaml + controller: nelmio_api_doc.controller.swagger_yaml \ No newline at end of file diff --git a/Tests/Functional/TestKernel.php b/Tests/Functional/TestKernel.php index d2aa13978..237f0790c 100644 --- a/Tests/Functional/TestKernel.php +++ b/Tests/Functional/TestKernel.php @@ -46,6 +46,8 @@ class TestKernel extends Kernel private $flags; + private bool $supportsLegacyAnnotations; + public function __construct(int $flags = 0) { parent::__construct('test'.$flags, true); @@ -61,18 +63,21 @@ public function registerBundles(): iterable $bundles = [ new FrameworkBundle(), new TwigBundle(), - new SensioFrameworkExtraBundle(), new ApiPlatformBundle(), new NelmioApiDocBundle(), new TestBundle(), - new FOSRestBundle(), ]; - if ($this->flags & self::USE_JMS) { - $bundles[] = new JMSSerializerBundle(); + if (self::MAJOR_VERSION < 7) { + $bundles[] = new SensioFrameworkExtraBundle(); + $bundles[] = new FOSRestBundle(); - if ($this->flags & self::USE_BAZINGA) { - $bundles[] = new BazingaHateoasBundle(); + if ($this->flags & self::USE_JMS) { + $bundles[] = new JMSSerializerBundle(); + + if ($this->flags & self::USE_BAZINGA) { + $bundles[] = new BazingaHateoasBundle(); + } } } @@ -84,28 +89,34 @@ public function registerBundles(): iterable */ protected function configureRoutes($routes) { - $this->import($routes, __DIR__.'/Resources/routes.yaml', '/', 'yaml'); + if (self::MAJOR_VERSION < 7) { + $this->import($routes, __DIR__.'/Resources/routes.yaml', '/', 'yaml'); + } else { + $this->import($routes, __DIR__.'/Resources/routes-attributes.yaml', '/', 'yaml'); + } if (class_exists(SerializedName::class)) { - $this->import($routes, __DIR__.'/Controller/SerializedNameController.php', '/', 'annotation'); + $this->import($routes, __DIR__.'/Controller/SerializedNameController.php', '/', self::MAJOR_VERSION < 7 ? 'annotation' : 'attribute'); } - if ($this->flags & self::USE_JMS) { - $this->import($routes, __DIR__.'/Controller/JMSController.php', '/', 'annotation'); - } + if (self::MAJOR_VERSION < 7) { + if ($this->flags & self::USE_JMS) { + $this->import($routes, __DIR__.'/Controller/JMSController.php', '/', 'annotation'); + } - if ($this->flags & self::USE_BAZINGA) { - $this->import($routes, __DIR__.'/Controller/BazingaController.php', '/', 'annotation'); + if ($this->flags & self::USE_BAZINGA) { + $this->import($routes, __DIR__.'/Controller/BazingaController.php', '/', 'annotation'); - try { - new \ReflectionMethod(Embedded::class, 'getType'); - $this->import($routes, __DIR__.'/Controller/BazingaTypedController.php', '/', 'annotation'); - } catch (\ReflectionException $e) { + try { + new \ReflectionMethod(Embedded::class, 'getType'); + $this->import($routes, __DIR__.'/Controller/BazingaTypedController.php', '/', 'annotation'); + } catch (\ReflectionException $e) { + } } - } - if ($this->flags & self::ERROR_ARRAY_ITEMS) { - $this->import($routes, __DIR__.'/Controller/ArrayItemsErrorController.php', '/', 'annotation'); + if ($this->flags & self::ERROR_ARRAY_ITEMS) { + $this->import($routes, __DIR__.'/Controller/ArrayItemsErrorController.php', '/', 'annotation'); + } } } @@ -132,8 +143,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'test' => null, 'validation' => null, 'form' => null, - 'serializer' => [ - 'enable_annotations' => true, + 'serializer' => self::MAJOR_VERSION < 7 ? ['enable_annotations' => true] : [] + [ 'mapping' => [ 'paths' => [__DIR__.'/Resources/serializer/'], ], @@ -156,12 +166,15 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'strict_variables' => '%kernel.debug%', 'exception_controller' => null, ]); + + if (self::MAJOR_VERSION < 7) { + $c->loadFromExtension('sensio_framework_extra', [ + 'router' => [ + 'annotations' => false, + ], + ]); + } - $c->loadFromExtension('sensio_framework_extra', [ - 'router' => [ - 'annotations' => false, - ], - ]); $c->loadFromExtension('api_platform', [ 'mapping' => ['paths' => [ @@ -171,28 +184,77 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ]], ]); - $c->loadFromExtension('fos_rest', [ - 'format_listener' => [ - 'rules' => [ - [ - 'path' => '^/', - 'fallback_format' => 'json', + if (self::MAJOR_VERSION < 7) { + $c->loadFromExtension('fos_rest', [ + 'format_listener' => [ + 'rules' => [ + [ + 'path' => '^/', + 'fallback_format' => 'json', + ], ], ], + ]); + + // If FOSRestBundle 2.8 + if (class_exists(\FOS\RestBundle\EventListener\ResponseStatusCodeListener::class)) { + $c->loadFromExtension('fos_rest', [ + 'exception' => [ + 'enabled' => false, + 'exception_listener' => false, + 'serialize_exceptions' => false, + ], + 'body_listener' => false, + 'routing_loader' => false, + ]); + } + } + + $models = [ + [ + 'alias' => 'PrivateProtectedExposure', + 'type' => PrivateProtectedExposure::class, ], - ]); + [ + 'alias' => 'SymfonyConstraintsTestGroup', + 'type' => SymfonyConstraintsWithValidationGroups::class, + 'groups' => ['test'], + ], + [ + 'alias' => 'SymfonyConstraintsDefaultGroup', + 'type' => SymfonyConstraintsWithValidationGroups::class, + 'groups' => null, + ], + ]; - // If FOSRestBundle 2.8 - if (class_exists(\FOS\RestBundle\EventListener\ResponseStatusCodeListener::class)) { - $c->loadFromExtension('fos_rest', [ - 'exception' => [ - 'enabled' => false, - 'exception_listener' => false, - 'serialize_exceptions' => false, + if (self::MAJOR_VERSION < 7) { + $models = [ + ...$models, + [ + 'alias' => 'JMSPicture_mini', + 'type' => JMSPicture::class, + 'groups' => ['mini'], ], - 'body_listener' => false, - 'routing_loader' => false, - ]); + [ + 'alias' => 'BazingaUser_grouped', + 'type' => BazingaUser::class, + 'groups' => ['foo'], + ], + [ + 'alias' => 'JMSComplex', + 'type' => JMSComplex::class, + 'groups' => [ + 'list', + 'details', + 'User' => ['list'], + ], + ], + [ + 'alias' => 'JMSComplexDefault', + 'type' => JMSComplex::class, + 'groups' => null, + ], + ]; } // Filter routes @@ -267,46 +329,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ], ], 'models' => [ - 'names' => [ - [ - 'alias' => 'PrivateProtectedExposure', - 'type' => PrivateProtectedExposure::class, - ], - [ - 'alias' => 'JMSPicture_mini', - 'type' => JMSPicture::class, - 'groups' => ['mini'], - ], - [ - 'alias' => 'BazingaUser_grouped', - 'type' => BazingaUser::class, - 'groups' => ['foo'], - ], - [ - 'alias' => 'JMSComplex', - 'type' => JMSComplex::class, - 'groups' => [ - 'list', - 'details', - 'User' => ['list'], - ], - ], - [ - 'alias' => 'JMSComplexDefault', - 'type' => JMSComplex::class, - 'groups' => null, - ], - [ - 'alias' => 'SymfonyConstraintsTestGroup', - 'type' => SymfonyConstraintsWithValidationGroups::class, - 'groups' => ['test'], - ], - [ - 'alias' => 'SymfonyConstraintsDefaultGroup', - 'type' => SymfonyConstraintsWithValidationGroups::class, - 'groups' => null, - ], - ], + 'names' => $models, ], ]); diff --git a/Tests/Functional/WebTestCase.php b/Tests/Functional/WebTestCase.php index 846c59a42..55724ae81 100644 --- a/Tests/Functional/WebTestCase.php +++ b/Tests/Functional/WebTestCase.php @@ -173,7 +173,7 @@ public function assertNotHasProperty($property, OA\AbstractAnnotation $annotatio /** * BC symfony < 5.3. */ - protected static function getContainer(): ContainerInterface + protected static function getContainer(): \Symfony\Component\DependencyInjection\Container { if (method_exists(parent::class, 'getContainer')) { return parent::getContainer(); diff --git a/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php b/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php index b84d27944..e9b42fe45 100644 --- a/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php +++ b/Tests/ModelDescriber/Annotations/AnnotationReaderTest.php @@ -38,7 +38,11 @@ public function testProperty($entity) $schema->merge([new OA\Property(['property' => 'property2'] + $baseProps)]); $registry = new ModelRegistry([], new OA\OpenApi($baseProps), []); - $symfonyConstraintAnnotationReader = new OpenApiAnnotationsReader(new AnnotationReader(), $registry, ['json']); + $symfonyConstraintAnnotationReader = new OpenApiAnnotationsReader( + class_exists(AnnotationReader::class) ? new AnnotationReader() : null, + $registry, + ['json'] + ); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->properties[1]); diff --git a/Tests/ModelDescriber/Annotations/Fixture/CompoundValidationRule.php b/Tests/ModelDescriber/Annotations/Fixture/CompoundValidationRule.php index bbfb42019..e6b5d3f74 100644 --- a/Tests/ModelDescriber/Annotations/Fixture/CompoundValidationRule.php +++ b/Tests/ModelDescriber/Annotations/Fixture/CompoundValidationRule.php @@ -14,6 +14,7 @@ class_alias(CompoundStub::class, Compound::class); /** * @Annotation */ +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] final class CompoundValidationRule extends Compound { protected function getConstraints(array $options): array diff --git a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php index 14a00741c..4e155cd76 100644 --- a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php +++ b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php @@ -16,6 +16,8 @@ use Nelmio\ApiDocBundle\Tests\Helper; use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert; use OpenApi\Annotations as OA; +use OpenApi\Attributes\Property; +use OpenApi\Attributes\Schema; use OpenApi\Context; use OpenApi\Generator; use PHPUnit\Framework\TestCase; @@ -24,6 +26,16 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase { + /** + * @var AnnotationReader|null + */ + private $doctrineAnnotations; + + protected function setUp(): void + { + $this->doctrineAnnotations = class_exists(AnnotationReader::class) ? new AnnotationReader() : null; + } + public function testUpdatePropertyFix1283() { $entity = new class() { @@ -42,7 +54,7 @@ public function testUpdatePropertyFix1283() $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property2'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -66,7 +78,7 @@ public function testOptionalProperty($entity) $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property2'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -110,7 +122,7 @@ public function testAssertChoiceResultsInNumericArray($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -152,7 +164,7 @@ public function testMultipleChoiceConstraintsApplyEnumToItems($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -188,7 +200,7 @@ public function testLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -224,7 +236,7 @@ public function testLengthConstraintDoesNotSetMinLengthIfMinIsNotSet($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -263,7 +275,7 @@ public function testCompoundValidationRules() $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => $propertyName])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, $propertyName), $schema->properties[0]); @@ -293,7 +305,7 @@ public function testCountConstraintDoesNotSetMinItemsIfMinIsNotSet($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -329,7 +341,7 @@ public function testCountConstraintDoesNotSetMaxItemsIfMaxIsNotSet($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -365,7 +377,7 @@ public function testRangeConstraintDoesNotSetMaximumIfMaxIsNotSet($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -401,7 +413,7 @@ public function testRangeConstraintDoesNotSetMinimumIfMinIsNotSet($entity) $schema = $this->createObj(OA\Schema::class, []); $schema->merge([$this->createObj(OA\Property::class, ['property' => 'property1'])]); - $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(new AnnotationReader()); + $symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($this->doctrineAnnotations); $symfonyConstraintAnnotationReader->setSchema($schema); $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); @@ -479,9 +491,9 @@ public function testReaderWithValidationGroupsEnabledDoesNotReadAnnotationsWitho */ public function testReaderWithValidationGroupsEnabledReadsOnlyConstraintsWithGroupsProvided($entity) { - $schema = $this->createObj(OA\Schema::class, []); + $schema = $this->createObj(Schema::class, []); $schema->merge([ - $this->createObj(OA\Property::class, ['property' => 'property1']), + $this->createObj(Property::class, ['property' => 'property1']), ]); $reader = $this->createConstraintReaderWithValidationGroupsEnabled(); $reader->setSchema($schema); @@ -543,7 +555,7 @@ public function provideConstraintsWithGroups(): iterable private function createConstraintReaderWithValidationGroupsEnabled(): SymfonyConstraintAnnotationReader { return new SymfonyConstraintAnnotationReader( - new AnnotationReader(), + $this->doctrineAnnotations, true ); } diff --git a/Tests/RouteDescriber/FosRestDescriberTest.php b/Tests/RouteDescriber/FosRestDescriberTest.php index 6219d718b..5d94b7b42 100644 --- a/Tests/RouteDescriber/FosRestDescriberTest.php +++ b/Tests/RouteDescriber/FosRestDescriberTest.php @@ -16,6 +16,7 @@ use Nelmio\ApiDocBundle\RouteDescriber\FosRestDescriber; use OpenApi\Annotations\OpenApi; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Route; use Symfony\Component\Validator\Constraints\Choice; @@ -23,15 +24,22 @@ class FosRestDescriberTest extends TestCase { public function testQueryParamWithChoiceConstraintIsAddedAsEnum() { + if (Kernel::MAJOR_VERSION >= 7) { + $this->markTestSkipped('FosRest is not supported in symfony 7'); + } + $choices = ['foo', 'bar']; $queryParam = new QueryParam(); $queryParam->requirements = new Choice($choices); - $readerMock = $this->createMock(Reader::class); - $readerMock->method('getMethodAnnotations')->willReturn([ - $queryParam, - ]); + $readerMock = null; + if (interface_exists(Reader::class)) { + $readerMock = $this->createMock(Reader::class); + $readerMock->method('getMethodAnnotations')->willReturn([ + $queryParam, + ]); + } $fosRestDescriber = new FosRestDescriber($readerMock, []); $api = new OpenApi([]); diff --git a/Tests/Routing/FilteredRouteCollectionBuilderTest.php b/Tests/Routing/FilteredRouteCollectionBuilderTest.php index f9eec1d6a..32cc91a98 100644 --- a/Tests/Routing/FilteredRouteCollectionBuilderTest.php +++ b/Tests/Routing/FilteredRouteCollectionBuilderTest.php @@ -31,6 +31,16 @@ */ class FilteredRouteCollectionBuilderTest extends TestCase { + /** + * @var AnnotationReader|null + */ + private $doctrineAnnotations; + + protected function setUp(): void + { + $this->doctrineAnnotations = class_exists(AnnotationReader::class) ? new AnnotationReader() : null; + } + public function testFilter() { $options = [ @@ -50,7 +60,7 @@ public function testFilter() } $routeBuilder = new FilteredRouteCollectionBuilder( - new AnnotationReader(), + $this->doctrineAnnotations, $this->createControllerReflector(), 'areaName', $options @@ -77,7 +87,7 @@ public function testFilterWithDeprecatedArgument() } $routeBuilder = new FilteredRouteCollectionBuilder( - new AnnotationReader(), + $this->doctrineAnnotations, $this->createControllerReflector(), 'areaName', $pathPattern @@ -95,7 +105,7 @@ public function testFilterWithInvalidOption(array $options) $this->expectException(InvalidArgumentException::class); new FilteredRouteCollectionBuilder( - new AnnotationReader(), + $this->doctrineAnnotations, $this->createControllerReflector(), 'areaName', $options @@ -148,7 +158,7 @@ public function testMatchingRoutes(string $name, Route $route, array $options = $routes->add($name, $route); $routeBuilder = new FilteredRouteCollectionBuilder( - new AnnotationReader(), + $this->doctrineAnnotations, $this->createControllerReflector(), 'area', $options @@ -188,12 +198,15 @@ public function testMatchingRoutesWithAnnotation(string $name, Route $route, arr $controllerReflectorStub = $this->createMock(ControllerReflector::class); $controllerReflectorStub->method('getReflectionMethod')->willReturn($reflectionMethodStub); - $annotationReader = $this->createMock(Reader::class); - $annotationReader - ->method('getMethodAnnotation') - ->with($reflectionMethodStub, Areas::class) - ->willReturn(new Areas(['value' => [$area]])) - ; + $annotationReader = null; + if (interface_exists(Reader::class)) { + $annotationReader = $this->createMock(Reader::class); + $annotationReader + ->method('getMethodAnnotation') + ->with($reflectionMethodStub, Areas::class) + ->willReturn(new Areas(['value' => [$area]])) + ; + } $routeBuilder = new FilteredRouteCollectionBuilder( $annotationReader, @@ -246,7 +259,7 @@ public function testNonMatchingRoutes(string $name, Route $route, array $options $routes->add($name, $route); $routeBuilder = new FilteredRouteCollectionBuilder( - new AnnotationReader(), + $this->doctrineAnnotations, $this->createControllerReflector(), 'areaName', $options @@ -289,11 +302,14 @@ public function testRoutesWithDisabledDefaultRoutes( $controllerReflectorStub = $this->createMock(ControllerReflector::class); $controllerReflectorStub->method('getReflectionMethod')->willReturn($reflectionMethodStub); - $annotationReader = $this->createMock(Reader::class); - $annotationReader - ->method('getMethodAnnotations') - ->willReturn($annotations) - ; + $annotationReader = null; + if (interface_exists(Reader::class)) { + $annotationReader = $this->createMock(Reader::class); + $annotationReader + ->method('getMethodAnnotations') + ->willReturn($annotations) + ; + } $routeBuilder = new FilteredRouteCollectionBuilder( $annotationReader, diff --git a/composer.json b/composer.json index e332fa6bb..f6425bd12 100644 --- a/composer.json +++ b/composer.json @@ -17,40 +17,38 @@ "require": { "php": ">=7.2", "ext-json": "*", - "doctrine/annotations": "^2.0", "psr/cache": "^1.0|^2.0|^3.0", "psr/container": "^1.0|^2.0", "psr/log": "^1.0|^2.0|^3.0", - "symfony/config": "^5.4|^6.0", - "symfony/console": "^5.4|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/framework-bundle": "^5.4.24|^6.0", - "symfony/http-foundation": "^5.4|^6.0", - "symfony/http-kernel": "^5.4|^6.0", - "symfony/options-resolver": "^5.4|^6.0", - "symfony/property-info": "^5.4|^6.0", - "symfony/routing": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4.24|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/options-resolver": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", "zircote/swagger-php": "^4.2.15", "phpdocumentor/reflection-docblock": "^3.1|^4.0|^5.0" }, "require-dev": { "sensio/framework-extra-bundle": "^5.4|^6.0", - "symfony/asset": "^5.4|^6.0", - "symfony/dom-crawler": "^5.4|^6.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/cache": "^5.4|^6.0", - "symfony/form": "^5.4|^6.0", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", "symfony/phpunit-bridge": "^6.3.2", - "symfony/property-access": "^5.4|^6.0", - "symfony/serializer": "^5.4|^6.0", - "symfony/stopwatch": "^5.4|^6.0", - "symfony/templating": "^5.4|^6.0", - "symfony/twig-bundle": "^5.4|^6.0", - "symfony/validator": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/templating": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/validator": "^5.4|^6.0|^7.0", "api-platform/core": "^2.7.0|^3", "symfony/deprecation-contracts": "^2.1|^3", - "friendsofsymfony/rest-bundle": "^2.8|^3.0", "willdurand/hateoas-bundle": "^1.0|^2.0", "jms/serializer-bundle": "^2.3|^3.0|^4.0|^5.0@beta", @@ -59,6 +57,7 @@ }, "suggest": { "api-platform/core": "For using an API oriented framework.", + "doctrine/annotations": "For using doctrine annotations", "friendsofsymfony/rest-bundle": "For using the parameters annotations.", "jms/serializer-bundle": "For describing your models.", "symfony/asset": "For using the Swagger UI.",