diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5500844e0..ac68b34e8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -24,17 +24,11 @@ jobs: strategy: matrix: include: - - php-version: 7.1 - composer-flags: "--prefer-lowest" - - php-version: 7.2 - symfony-require: "^4.0" - - php-version: 7.3 - symfony-require: "^5.0" - - php-version: 7.4 - symfony-require: "^4.0" - - php-version: 7.3 - symfony-require: "^5.0" - php-version: 8.0 + symfony-require: "^5.3" + - php-version: 8.0 + symfony-require: "^6.0" + - php-version: 8.1 composer-flags: "--ignore-platform-reqs" steps: diff --git a/Annotation/Areas.php b/Annotation/Areas.php index f4ce2f9da..821469812 100644 --- a/Annotation/Areas.php +++ b/Annotation/Areas.php @@ -17,7 +17,7 @@ final class Areas { /** @var string[] */ - private $areas; + private array $areas; public function __construct(array $properties) { diff --git a/Annotation/Model.php b/Annotation/Model.php index 5a258292a..f9751c42a 100644 --- a/Annotation/Model.php +++ b/Annotation/Model.php @@ -32,18 +32,15 @@ final class Model extends AbstractAnnotation Parameter::class, ]; - /** - * @var string - */ - public $type; + public string $type; /** * @var string[] */ - public $groups; + public array $groups; /** * @var mixed[] */ - public $options; + public array $options; } diff --git a/Annotation/Security.php b/Annotation/Security.php index 95bbc4b98..7d3b6793d 100644 --- a/Annotation/Security.php +++ b/Annotation/Security.php @@ -26,13 +26,10 @@ class Security extends AbstractAnnotation public static $_required = ['name']; - /** - * @var string - */ - public $name; + public string $name; /** * @var string[] */ - public $scopes = []; + public array $scopes = []; } diff --git a/ApiDocGenerator.php b/ApiDocGenerator.php index 57b105987..0fbfe9ccf 100644 --- a/ApiDocGenerator.php +++ b/ApiDocGenerator.php @@ -20,44 +20,33 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Analysis; use OpenApi\Annotations\OpenApi; +use OpenApi\Generator; use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerAwareTrait; final class ApiDocGenerator { use LoggerAwareTrait; - /** @var OpenApi */ - private $openApi; - - /** @var iterable|DescriberInterface[] */ - private $describers; - - /** @var iterable|ModelDescriberInterface[] */ - private $modelDescribers; - - /** @var CacheItemPoolInterface|null */ - private $cacheItemPool; - - /** @var string|null */ - private $cacheItemId; + private ?OpenApi $openApi = null; /** @var string[] */ - private $alternativeNames = []; + private array $alternativeNames = []; /** @var string[] */ - private $mediaTypes = ['json']; + private array $mediaTypes = ['json']; /** - * @param DescriberInterface[]|iterable $describers + * @param iterable|DescriberInterface[] $describers * @param ModelDescriberInterface[]|iterable $modelDescribers */ - public function __construct($describers, $modelDescribers, CacheItemPoolInterface $cacheItemPool = null, string $cacheItemId = null) - { - $this->describers = $describers; - $this->modelDescribers = $modelDescribers; - $this->cacheItemPool = $cacheItemPool; - $this->cacheItemId = $cacheItemId; + public function __construct( + private iterable $describers, + private iterable $modelDescribers, + private ?CacheItemPoolInterface $cacheItemPool = null, + private ?string $cacheItemId = null + ) { } public function setAlternativeNames(array $alternativeNames) @@ -70,6 +59,9 @@ public function setMediaTypes(array $mediaTypes) $this->mediaTypes = $mediaTypes; } + /** + * @throws InvalidArgumentException + */ public function generate(): OpenApi { if (null !== $this->openApi) { @@ -110,7 +102,7 @@ public function generate(): OpenApi $defaultOperationIdProcessor = new DefaultOperationId(); $defaultOperationIdProcessor($analysis); - $analysis->process(); + $analysis->process((new Generator())->getProcessors()); $analysis->validate(); if (isset($item)) { diff --git a/Command/DumpCommand.php b/Command/DumpCommand.php index c2d929fac..b48d788cd 100644 --- a/Command/DumpCommand.php +++ b/Command/DumpCommand.php @@ -20,23 +20,13 @@ class DumpCommand extends Command { - /** - * @var RenderOpenApi - */ - private $renderOpenApi; - - /** - * @var mixed[] - */ - private $defaultHtmlConfig = [ + private array $defaultHtmlConfig = [ 'assets_mode' => AssetsMode::CDN, 'swagger_ui_config' => [], ]; - public function __construct(RenderOpenApi $renderOpenApi) + public function __construct(private RenderOpenApi $renderOpenApi) { - $this->renderOpenApi = $renderOpenApi; - parent::__construct(); } @@ -62,10 +52,7 @@ protected function configure() ; } - /** - * @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/Controller/DocumentationController.php b/Controller/DocumentationController.php index 020451a8a..773e0e463 100644 --- a/Controller/DocumentationController.php +++ b/Controller/DocumentationController.php @@ -18,23 +18,17 @@ final class DocumentationController { - /** - * @var RenderOpenApi - */ - private $renderOpenApi; - - public function __construct(RenderOpenApi $renderOpenApi) + public function __construct(private RenderOpenApi $renderOpenApi) { - $this->renderOpenApi = $renderOpenApi; } - public function __invoke(Request $request, $area = 'default') + public function __invoke(Request $request, $area = 'default'): JsonResponse { try { return JsonResponse::fromJsonString( $this->renderOpenApi->renderFromRequest($request, RenderOpenApi::JSON, $area) ); - } catch (InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area)); } } diff --git a/Controller/SwaggerUiController.php b/Controller/SwaggerUiController.php index c5a32017d..f191c9303 100644 --- a/Controller/SwaggerUiController.php +++ b/Controller/SwaggerUiController.php @@ -11,7 +11,6 @@ namespace Nelmio\ApiDocBundle\Controller; -use InvalidArgumentException; use Nelmio\ApiDocBundle\Render\Html\AssetsMode; use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Symfony\Component\HttpFoundation\Request; @@ -20,17 +19,11 @@ final class SwaggerUiController { - /** - * @var RenderOpenApi - */ - private $renderOpenApi; - - public function __construct(RenderOpenApi $renderOpenApi) + public function __construct(private RenderOpenApi $renderOpenApi) { - $this->renderOpenApi = $renderOpenApi; } - public function __invoke(Request $request, $area = 'default') + public function __invoke(Request $request, $area = 'default'): Response { try { $response = new Response( @@ -42,9 +35,9 @@ public function __invoke(Request $request, $area = 'default') ); return $response->setCharset('UTF-8'); - } catch (InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { $advice = ''; - if (false !== strpos($area, '.json')) { + if (str_contains($area, '.json')) { $advice = ' Since the area provided contains `.json`, the issue is likely caused by route priorities. Try switching the Swagger UI / the json documentation routes order.'; } diff --git a/Controller/YamlDocumentationController.php b/Controller/YamlDocumentationController.php index a935a2d4c..d17602012 100644 --- a/Controller/YamlDocumentationController.php +++ b/Controller/YamlDocumentationController.php @@ -11,7 +11,6 @@ namespace Nelmio\ApiDocBundle\Controller; -use InvalidArgumentException; use Nelmio\ApiDocBundle\Render\RenderOpenApi; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -19,17 +18,11 @@ final class YamlDocumentationController { - /** - * @var RenderOpenApi - */ - private $renderOpenApi; - - public function __construct(RenderOpenApi $renderOpenApi) + public function __construct(private RenderOpenApi $renderOpenApi) { - $this->renderOpenApi = $renderOpenApi; } - public function __invoke(Request $request, $area = 'default') + public function __invoke(Request $request, $area = 'default'): Response { try { $response = new Response( @@ -39,7 +32,7 @@ public function __invoke(Request $request, $area = 'default') ); return $response->setCharset('UTF-8'); - } catch (InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { throw new BadRequestHttpException(sprintf('Area "%s" is not supported as it isn\'t defined in config.', $area)); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 5a4c2e3a5..64befc8bb 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -16,16 +16,10 @@ final class Configuration implements ConfigurationInterface { - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('nelmio_api_doc'); - - if (method_exists($treeBuilder, 'getRootNode')) { - $rootNode = $treeBuilder->getRootNode(); - } else { - // symfony < 4.2 support - $rootNode = $treeBuilder->root('nelmio_api_doc'); - } + $rootNode = $treeBuilder->getRootNode(); $rootNode ->children() diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index cd6a622d8..2040acbc7 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -152,14 +152,15 @@ public function load(array $configs, ContainerBuilder $container) ->setArgument(1, $config['media_types']); } - // ApiPlatform support $bundles = $container->getParameter('kernel.bundles'); - if (!isset($bundles['TwigBundle'])) { + if (!isset($bundles['TwigBundle']) || !class_exists('Symfony\Component\Asset\Packages')) { $container->removeDefinition('nelmio_api_doc.controller.swagger_ui'); $container->removeDefinition('nelmio_api_doc.render_docs.html'); $container->removeDefinition('nelmio_api_doc.render_docs.html.asset'); } + + // ApiPlatform support if (isset($bundles['ApiPlatformBundle']) && class_exists('ApiPlatform\Core\Documentation\Documentation')) { $loader->load('api_platform.xml'); } diff --git a/Describer/ApiPlatformDescriber.php b/Describer/ApiPlatformDescriber.php index 2b837adde..fd46f3b89 100644 --- a/Describer/ApiPlatformDescriber.php +++ b/Describer/ApiPlatformDescriber.php @@ -20,7 +20,7 @@ final class ApiPlatformDescriber extends ExternalDocDescriber public function __construct(Documentation $documentation, NormalizerInterface $normalizer) { if (!$normalizer->supportsNormalization($documentation, 'json')) { - throw new \InvalidArgumentException(sprintf('Argument 2 passed to %s() must implement %s and support normalization of %s. The normalizer provided is an instance of %s.', __METHOD__, NormalizerInterface::class, Documentation::class, get_class($normalizer))); + throw new \InvalidArgumentException(sprintf('Argument 2 passed to %s() must implement %s and support normalization of %s. The normalizer provided is an instance of %s.', __METHOD__, NormalizerInterface::class, Documentation::class, $normalizer::class)); } parent::__construct(function () use ($documentation, $normalizer) { diff --git a/Describer/DefaultDescriber.php b/Describer/DefaultDescriber.php index 5217d3c11..3812e3a03 100644 --- a/Describer/DefaultDescriber.php +++ b/Describer/DefaultDescriber.php @@ -13,6 +13,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; /** * Makes the swagger documentation valid even if there are missing fields. @@ -26,22 +27,22 @@ public function describe(OA\OpenApi $api) // Info /** @var OA\Info $info */ $info = Util::getChild($api, OA\Info::class); - if (OA\UNDEFINED === $info->title) { + if (Generator::UNDEFINED === $info->title) { $info->title = ''; } - if (OA\UNDEFINED === $info->version) { + if (Generator::UNDEFINED === $info->version) { $info->version = '0.0.0'; } // Paths - if (OA\UNDEFINED === $api->paths) { + if (Generator::UNDEFINED === $api->paths) { $api->paths = []; } foreach ($api->paths as $path) { foreach (Util::OPERATIONS as $method) { /** @var OA\Operation $operation */ $operation = $path->{$method}; - if (OA\UNDEFINED !== $operation && null !== $operation && (OA\UNDEFINED === $operation->responses || empty($operation->responses))) { + if (Generator::UNDEFINED !== $operation && null !== $operation && (Generator::UNDEFINED === $operation->responses || empty($operation->responses))) { /** @var OA\Response $response */ $response = Util::getIndexedCollectionItem($operation, OA\Response::class, 'default'); $response->description = ''; diff --git a/Describer/ExternalDocDescriber.php b/Describer/ExternalDocDescriber.php index e023a3349..edfaa83c2 100644 --- a/Describer/ExternalDocDescriber.php +++ b/Describer/ExternalDocDescriber.php @@ -18,15 +18,9 @@ class ExternalDocDescriber implements DescriberInterface { private $externalDoc; - private $overwrite; - - /** - * @param array|callable $externalDoc - */ - public function __construct($externalDoc, bool $overwrite = false) + public function __construct(callable|array $externalDoc, private bool $overwrite = false) { $this->externalDoc = $externalDoc; - $this->overwrite = $overwrite; } public function describe(OA\OpenApi $api) diff --git a/Describer/ModelRegistryAwareTrait.php b/Describer/ModelRegistryAwareTrait.php index 3bcdecac3..28eaaabe9 100644 --- a/Describer/ModelRegistryAwareTrait.php +++ b/Describer/ModelRegistryAwareTrait.php @@ -15,10 +15,7 @@ trait ModelRegistryAwareTrait { - /** - * @var ModelRegistry - */ - private $modelRegistry; + private ModelRegistry $modelRegistry; public function setModelRegistry(ModelRegistry $modelRegistry) { diff --git a/Describer/OpenApiPhpDescriber.php b/Describer/OpenApiPhpDescriber.php index 96aad2dc0..76160dac8 100644 --- a/Describer/OpenApiPhpDescriber.php +++ b/Describer/OpenApiPhpDescriber.php @@ -16,8 +16,9 @@ use Nelmio\ApiDocBundle\Annotation\Security; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\Util\ControllerReflector; -use OpenApi\Analyser; +use Nelmio\ApiDocBundle\Util\SetsContextTrait; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Psr\Log\LoggerInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -27,21 +28,20 @@ class_exists(OA\OpenApi::class); final class OpenApiPhpDescriber { - private $routeCollection; - private $controllerReflector; - private $annotationReader; - private $logger; - private $overwrite; - - public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, Reader $annotationReader, LoggerInterface $logger, bool $overwrite = false) - { - $this->routeCollection = $routeCollection; - $this->controllerReflector = $controllerReflector; - $this->annotationReader = $annotationReader; - $this->logger = $logger; - $this->overwrite = $overwrite; + use SetsContextTrait; + + public function __construct( + private RouteCollection $routeCollection, + private ControllerReflector $controllerReflector, + private Reader $annotationReader, + private LoggerInterface $logger, + private bool $overwrite = false + ) { } + /** + * @throws \Exception + */ public function describe(OA\OpenApi $api) { $classAnnotations = []; @@ -52,11 +52,13 @@ public function describe(OA\OpenApi $api) $path = Util::getPath($api, $path); - Analyser::$context = Util::createContext(['nested' => $path], $path->_context); - Analyser::$context->namespace = $method->getNamespaceName(); - Analyser::$context->class = $declaringClass->getShortName(); - Analyser::$context->method = $method->name; - Analyser::$context->filename = $method->getFileName(); + $context = Util::createContext(['nested' => $path], $path->_context); + $context->namespace = $method->getNamespaceName(); + $context->class = $declaringClass->getShortName(); + $context->method = $method->name; + $context->filename = $method->getFileName(); + + $this->setContext($context); if (!array_key_exists($declaringClass->getName(), $classAnnotations)) { $classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) { @@ -90,7 +92,7 @@ public function describe(OA\OpenApi $api) if (!in_array($annotation->method, $httpMethods, true)) { continue; } - if (OA\UNDEFINED !== $annotation->path && $path->path !== $annotation->path) { + if (Generator::UNDEFINED !== $annotation->path && $path->path !== $annotation->path) { continue; } @@ -120,7 +122,7 @@ public function describe(OA\OpenApi $api) !$annotation instanceof OA\Parameter && !$annotation instanceof OA\ExternalDocumentation ) { - throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed.', get_class($annotation), $method->getDeclaringClass()->name, $method->name)); + throw new \LogicException(sprintf('Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed.', $annotation::class, $method->getDeclaringClass()->name, $method->name)); } $implicitAnnotations[] = $annotation; @@ -135,14 +137,14 @@ public function describe(OA\OpenApi $api) $operation->merge($implicitAnnotations); $operation->mergeProperties($mergeProperties); - if (OA\UNDEFINED === $operation->operationId) { + if (Generator::UNDEFINED === $operation->operationId) { $operation->operationId = $httpMethod.'_'.$routeName; } } } - // Reset the Analyser after the parsing - Analyser::$context = null; + // Reset the Generator after the parsing + $this->setContext(null); } private function getMethodsToParse(): \Generator @@ -179,7 +181,7 @@ private function getSupportedHttpMethods(Route $route): array private function normalizePath(string $path): string { - if ('.{_format}' === substr($path, -10)) { + if (str_ends_with($path, '.{_format}')) { $path = substr($path, 0, -10); } diff --git a/Describer/RouteDescriber.php b/Describer/RouteDescriber.php index 00b7ba729..a8c625a81 100644 --- a/Describer/RouteDescriber.php +++ b/Describer/RouteDescriber.php @@ -20,20 +20,14 @@ final class RouteDescriber implements DescriberInterface, ModelRegistryAwareInte { use ModelRegistryAwareTrait; - private $routeCollection; - - private $controllerReflector; - - private $routeDescribers; - /** * @param RouteDescriberInterface[]|iterable $routeDescribers */ - public function __construct(RouteCollection $routeCollection, ControllerReflector $controllerReflector, $routeDescribers) - { - $this->routeCollection = $routeCollection; - $this->controllerReflector = $controllerReflector; - $this->routeDescribers = $routeDescribers; + public function __construct( + private RouteCollection $routeCollection, + private ControllerReflector $controllerReflector, + private iterable $routeDescribers + ) { } public function describe(OA\OpenApi $api) diff --git a/Exception/UndocumentedArrayItemsException.php b/Exception/UndocumentedArrayItemsException.php index bedfcc27c..e875b481d 100644 --- a/Exception/UndocumentedArrayItemsException.php +++ b/Exception/UndocumentedArrayItemsException.php @@ -13,14 +13,8 @@ class UndocumentedArrayItemsException extends \LogicException { - private $class; - private $path; - - public function __construct(string $class = null, string $path = '') + public function __construct(private ?string $class = null, private string $path = '') { - $this->class = $class; - $this->path = $path; - $propertyName = ''; if (null !== $class) { $propertyName = $class.'::'; @@ -30,12 +24,12 @@ public function __construct(string $class = null, string $path = '') parent::__construct(sprintf('Property "%s" is an array, but its items type isn\'t specified. You can specify that by using the type `string[]` for instance or `@OA\Property(type="array", @OA\Items(type="string"))`.', $propertyName)); } - public function getClass() + public function getClass(): ?string { return $this->class; } - public function getPath() + public function getPath(): string { return $this->path; } diff --git a/Form/Extension/DocumentationExtension.php b/Form/Extension/DocumentationExtension.php index 2cc03a4b7..858b41c66 100644 --- a/Form/Extension/DocumentationExtension.php +++ b/Form/Extension/DocumentationExtension.php @@ -32,7 +32,7 @@ public function configureOptions(OptionsResolver $resolver) ->setAllowedTypes('documentation', ['array', 'bool']); } - public function getExtendedType() + public function getExtendedType(): FormType { return self::getExtendedTypes()[0]; } diff --git a/Model/Model.php b/Model/Model.php index a575bfef3..364eb3899 100644 --- a/Model/Model.php +++ b/Model/Model.php @@ -15,26 +15,14 @@ final class Model { - private $type; - - private $groups; - - private $options; - /** * @param string[]|null $groups */ - public function __construct(Type $type, array $groups = null, array $options = null) + public function __construct(private Type $type, private ?array $groups = null, private ?array $options = null) { - $this->type = $type; - $this->groups = $groups; - $this->options = $options; } - /** - * @return Type - */ - public function getType() + public function getType(): Type { return $this->type; } @@ -42,7 +30,7 @@ public function getType() /** * @return string[]|null */ - public function getGroups() + public function getGroups(): ?array { return $this->groups; } @@ -52,10 +40,7 @@ public function getHash(): string return md5(serialize([$this->type, $this->groups])); } - /** - * @return mixed[]|null - */ - public function getOptions() + public function getOptions(): ?array { return $this->options; } diff --git a/Model/ModelRegistry.php b/Model/ModelRegistry.php index db346fb87..4ca3c3ed2 100644 --- a/Model/ModelRegistry.php +++ b/Model/ModelRegistry.php @@ -23,29 +23,22 @@ final class ModelRegistry { use LoggerAwareTrait; - private $registeredModelNames = []; - - private $alternativeNames = []; - - private $unregistered = []; - - private $models = []; - - private $names = []; - - private $modelDescribers = []; - - private $api; + private array $registeredModelNames = []; + private array $alternativeNames = []; + private array $unregistered = []; + private array $models = []; + private array $names = []; /** * @param ModelDescriberInterface[]|iterable $modelDescribers * * @internal */ - public function __construct($modelDescribers, OA\OpenApi $api, array $alternativeNames = []) - { - $this->modelDescribers = $modelDescribers; - $this->api = $api; + public function __construct( + private iterable $modelDescribers, + private OA\OpenApi $api, + array $alternativeNames = [] + ) { $this->logger = new NullLogger(); foreach (array_reverse($alternativeNames) as $alternativeName => $criteria) { $this->alternativeNames[] = $model = new Model(new Type('object', false, $criteria['type']), $criteria['groups']); @@ -188,31 +181,16 @@ private function typeToString(Type $type): string private function getCollectionKeyTypes(Type $type): array { - // BC layer, this condition should be removed after removing support for symfony < 5.3 - if (!method_exists($type, 'getCollectionKeyTypes')) { - return null !== $type->getCollectionKeyType() ? [$type->getCollectionKeyType()] : []; - } - return $type->getCollectionKeyTypes(); } private function getCollectionValueTypes(Type $type): array { - // BC layer, this condition should be removed after removing support for symfony < 5.3 - if (!method_exists($type, 'getCollectionValueTypes')) { - return null !== $type->getCollectionValueType() ? [$type->getCollectionValueType()] : []; - } - return $type->getCollectionValueTypes(); } private function getCollectionValueType(Type $type): ?Type { - // BC layer, this condition should be removed after removing support for symfony < 5.3 - if (!method_exists($type, 'getCollectionValueTypes')) { - return $type->getCollectionValueType(); - } - return $type->getCollectionValueTypes()[0] ?? null; } } diff --git a/ModelDescriber/Annotations/AnnotationsReader.php b/ModelDescriber/Annotations/AnnotationsReader.php index 62641abba..8489f94fd 100644 --- a/ModelDescriber/Annotations/AnnotationsReader.php +++ b/ModelDescriber/Annotations/AnnotationsReader.php @@ -20,18 +20,12 @@ */ class AnnotationsReader { - private $annotationsReader; - private $modelRegistry; - - private $phpDocReader; - private $openApiAnnotationsReader; - private $symfonyConstraintAnnotationReader; + private PropertyPhpDocReader $phpDocReader; + private OpenApiAnnotationsReader $openApiAnnotationsReader; + private SymfonyConstraintAnnotationReader $symfonyConstraintAnnotationReader; public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes) { - $this->annotationsReader = $annotationsReader; - $this->modelRegistry = $modelRegistry; - $this->phpDocReader = new PropertyPhpDocReader(); $this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes); $this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader($annotationsReader); diff --git a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php index 020b8b11b..de5724c50 100644 --- a/ModelDescriber/Annotations/OpenApiAnnotationsReader.php +++ b/ModelDescriber/Annotations/OpenApiAnnotationsReader.php @@ -15,22 +15,23 @@ use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use OpenApi\Analyser; +use Nelmio\ApiDocBundle\Util\SetsContextTrait; use OpenApi\Analysis; use OpenApi\Annotations as OA; use OpenApi\Context; +use OpenApi\Generator; /** * @internal */ class OpenApiAnnotationsReader { - private $annotationsReader; - private $modelRegister; + use SetsContextTrait; - public function __construct(Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes) + private ModelRegister $modelRegister; + + public function __construct(private Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes) { - $this->annotationsReader = $annotationsReader; $this->modelRegister = new ModelRegister($modelRegistry, $mediaTypes); } @@ -60,19 +61,20 @@ public function getPropertyName($reflection, string $default): string return $default; } - return OA\UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default; + return Generator::UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default; } public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void { // In order to have nicer errors $declaringClass = $reflection->getDeclaringClass(); - Analyser::$context = new Context([ + + $this->setContext(new Context([ 'namespace' => $declaringClass->getNamespaceName(), 'class' => $declaringClass->getShortName(), 'property' => $reflection->name, 'filename' => $declaringClass->getFileName(), - ]); + ])); /** @var OA\Property $oaProperty */ if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) { @@ -80,7 +82,7 @@ public function updateProperty($reflection, OA\Property $property, array $serial } elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) { return; } - Analyser::$context = null; + $this->setContext(null); // Read @Model annotations $this->modelRegister->__invoke(new Analysis([$oaProperty], Util::createContext()), $serializationGroups); diff --git a/ModelDescriber/Annotations/PropertyPhpDocReader.php b/ModelDescriber/Annotations/PropertyPhpDocReader.php index 5234b45b0..16cf7e2bb 100644 --- a/ModelDescriber/Annotations/PropertyPhpDocReader.php +++ b/ModelDescriber/Annotations/PropertyPhpDocReader.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\ModelDescriber\Annotations; use OpenApi\Annotations as OA; +use OpenApi\Generator; use phpDocumentor\Reflection\DocBlock\Tags\Var_; use phpDocumentor\Reflection\DocBlockFactory; @@ -22,7 +23,7 @@ */ class PropertyPhpDocReader { - private $docBlockFactory; + private DocBlockFactory $docBlockFactory; public function __construct() { @@ -36,7 +37,7 @@ public function updateProperty($reflection, OA\Property $property): void { try { $docBlock = $this->docBlockFactory->create($reflection); - } catch (\Exception $e) { + } catch (\Exception) { // ignore return; } @@ -53,10 +54,10 @@ public function updateProperty($reflection, OA\Property $property): void } } } - if (OA\UNDEFINED === $property->title && $title) { + if (Generator::UNDEFINED === $property->title && $title) { $property->title = $title; } - if (OA\UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) { + if (Generator::UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) { $property->description = $docBlock->getDescription()->render(); } } diff --git a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php index 8fa31f6a7..d16d37ee5 100644 --- a/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php +++ b/ModelDescriber/Annotations/SymfonyConstraintAnnotationReader.php @@ -14,6 +14,7 @@ use Doctrine\Common\Annotations\Reader; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints as Assert; @@ -22,27 +23,16 @@ */ class SymfonyConstraintAnnotationReader { - /** - * @var Reader - */ - private $annotationsReader; - - /** - * @var OA\Schema - */ - private $schema; + private OA\Schema $schema; - public function __construct(Reader $annotationsReader) + public function __construct(private Reader $annotationsReader) { - $this->annotationsReader = $annotationsReader; } /** * Update the given property and schema with defined Symfony constraints. - * - * @param \ReflectionProperty|\ReflectionMethod $reflection */ - public function updateProperty($reflection, OA\Property $property): void + public function updateProperty(\ReflectionMethod|\ReflectionProperty $reflection, OA\Property $property): void { foreach ($this->getAnnotations($reflection) as $outerAnnotation) { $innerAnnotations = $outerAnnotation instanceof Assert\Compound @@ -73,7 +63,7 @@ private function processPropertyAnnotations($reflection, OA\Property $property, return; } - $existingRequiredFields = OA\UNDEFINED !== $this->schema->required ? $this->schema->required : []; + $existingRequiredFields = Generator::UNDEFINED !== $this->schema->required ? $this->schema->required : []; $existingRequiredFields[] = $propertyName; $this->schema->required = array_values(array_unique($existingRequiredFields)); @@ -131,7 +121,7 @@ private function getSchemaPropertyName(OA\Schema $property): ?string } foreach ($this->schema->properties as $schemaProperty) { if ($schemaProperty === $property) { - return OA\UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null; + return Generator::UNDEFINED !== $schemaProperty->property ? $schemaProperty->property : null; } } @@ -146,17 +136,14 @@ private function appendPattern(OA\Schema $property, $newPattern): void if (null === $newPattern) { return; } - if (OA\UNDEFINED !== $property->pattern) { + if (Generator::UNDEFINED !== $property->pattern) { $property->pattern = sprintf('%s, %s', $property->pattern, $newPattern); } else { $property->pattern = $newPattern; } } - /** - * @param \ReflectionProperty|\ReflectionMethod $reflection - */ - private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, $reflection): void + private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, \ReflectionMethod|\ReflectionProperty $reflection): void { if ($choice->callback) { $enumValues = call_user_func(is_array($choice->callback) ? $choice->callback : [$reflection->class, $choice->callback]); @@ -172,10 +159,7 @@ private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choic $setEnumOnThis->enum = array_values($enumValues); } - /** - * @param \ReflectionProperty|\ReflectionMethod $reflection - */ - private function getAnnotations($reflection): \Traversable + private function getAnnotations(\ReflectionMethod|\ReflectionProperty $reflection): \Traversable { if (\PHP_VERSION_ID >= 80000 && class_exists(Constraint::class)) { foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { diff --git a/ModelDescriber/BazingaHateoasModelDescriber.php b/ModelDescriber/BazingaHateoasModelDescriber.php index 35e0eee32..3b5a0ba55 100644 --- a/ModelDescriber/BazingaHateoasModelDescriber.php +++ b/ModelDescriber/BazingaHateoasModelDescriber.php @@ -14,6 +14,8 @@ use Hateoas\Configuration\Metadata\ClassMetadata; use Hateoas\Configuration\Relation; use Hateoas\Serializer\Metadata\RelationPropertyMetadata; +use Metadata\ClassHierarchyMetadata; +use Metadata\MergeableClassMetadata; use Metadata\MetadataFactoryInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface; use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; @@ -26,13 +28,8 @@ class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegi { use ModelRegistryAwareTrait; - private $factory; - private $JMSModelDescriber; - - public function __construct(MetadataFactoryInterface $factory, JMSModelDescriber $JMSModelDescriber) + public function __construct(private MetadataFactoryInterface $factory, private JMSModelDescriber $JMSModelDescriber) { - $this->factory = $factory; - $this->JMSModelDescriber = $JMSModelDescriber; } public function setModelRegistry(ModelRegistry $modelRegistry) @@ -92,7 +89,7 @@ public function describe(Model $model, OA\Schema $schema): void } } - private function getHateoasMetadata(Model $model) + private function getHateoasMetadata(Model $model): MergeableClassMetadata|ClassHierarchyMetadata|null { $className = $model->getType()->getClassName(); @@ -100,7 +97,7 @@ private function getHateoasMetadata(Model $model) if ($metadata = $this->factory->getMetadataForClass($className)) { return $metadata; } - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { } return null; diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 6bff0e443..30e5824cd 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormConfigInterface; @@ -34,15 +35,14 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry { use ModelRegistryAwareTrait; - private $formFactory; - private $doctrineReader; - private $mediaTypes; + private ?array $mediaTypes; - public function __construct(FormFactoryInterface $formFactory = null, Reader $reader = null, array $mediaTypes = null) - { - $this->formFactory = $formFactory; - $this->doctrineReader = $reader; - if (null === $reader) { + public function __construct( + private ?FormFactoryInterface $formFactory = null, + private ?Reader $doctrineReader = null, + array $mediaTypes = null + ) { + if (null === $doctrineReader) { @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); } @@ -53,6 +53,9 @@ public function __construct(FormFactoryInterface $formFactory = null, Reader $re $this->mediaTypes = $mediaTypes; } + /** + * @throws \ReflectionException + */ public function describe(Model $model, OA\Schema $schema) { if (method_exists(AbstractType::class, 'setDefaultOptions')) { @@ -90,7 +93,7 @@ private function parseForm(OA\Schema $schema, FormInterface $form) $property = Util::getProperty($schema, $name); if ($config->getRequired()) { - $required = OA\UNDEFINED !== $schema->required ? $schema->required : []; + $required = Generator::UNDEFINED !== $schema->required ? $schema->required : []; $required[] = $name; $schema->required = $required; @@ -100,7 +103,7 @@ private function parseForm(OA\Schema $schema, FormInterface $form) $property->mergeProperties($config->getOption('documentation')); } - if (OA\UNDEFINED !== $property->type) { + if (Generator::UNDEFINED !== $property->type) { continue; // Type manually defined } @@ -120,7 +123,7 @@ private function findFormType(FormConfigInterface $config, OA\Schema $property) if (!$builtinFormType = $this->getBuiltinFormType($type)) { // if form type is not builtin in Form component. $model = new Model( - new Type(Type::BUILTIN_TYPE_OBJECT, false, get_class($type->getInnerType())), + new Type(Type::BUILTIN_TYPE_OBJECT, false, $type->getInnerType()::class), null, $config->getOptions() ); @@ -285,13 +288,10 @@ private function isBooleansArray(array $array): bool return true; } - /** - * @return ResolvedFormTypeInterface|null - */ - private function getBuiltinFormType(ResolvedFormTypeInterface $type) + private function getBuiltinFormType(ResolvedFormTypeInterface $type): ?ResolvedFormTypeInterface { do { - $class = get_class($type->getInnerType()); + $class = $type->getInnerType()::class; if (FormType::class === $class) { return null; @@ -301,7 +301,7 @@ private function getBuiltinFormType(ResolvedFormTypeInterface $type) return $type; } - if (0 === strpos($class, 'Symfony\Component\Form\Extension\Core\Type\\')) { + if (str_starts_with($class, 'Symfony\Component\Form\Extension\Core\Type\\')) { return $type; } } while ($type = $type->getParent()); diff --git a/ModelDescriber/JMSModelDescriber.php b/ModelDescriber/JMSModelDescriber.php index c81519abd..b6a0ab279 100644 --- a/ModelDescriber/JMSModelDescriber.php +++ b/ModelDescriber/JMSModelDescriber.php @@ -14,6 +14,8 @@ use Doctrine\Common\Annotations\Reader; use JMS\Serializer\Context; use JMS\Serializer\Exclusion\GroupsExclusionStrategy; +use JMS\Serializer\Metadata\ClassMetadata; +use JMS\Serializer\Metadata\PropertyMetadata; use JMS\Serializer\Naming\PropertyNamingStrategyInterface; use JMS\Serializer\SerializationContext; use Metadata\MetadataFactoryInterface; @@ -23,6 +25,7 @@ use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\PropertyInfo\Type; /** @@ -32,33 +35,18 @@ class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareIn { use ModelRegistryAwareTrait; - private $factory; + private array $contexts = []; - private $namingStrategy; + private array $metadataStacks = []; - private $doctrineReader; - - private $contexts = []; - - private $metadataStacks = []; - - private $mediaTypes; - - /** - * @var array - */ - private $propertyTypeUseGroupsCache = []; + private array $propertyTypeUseGroupsCache = []; public function __construct( - MetadataFactoryInterface $factory, - Reader $reader, - array $mediaTypes, - ?PropertyNamingStrategyInterface $namingStrategy = null + private MetadataFactoryInterface $factory, + private Reader $doctrineReader, + private array $mediaTypes, + private ?PropertyNamingStrategyInterface $namingStrategy = null ) { - $this->factory = $factory; - $this->namingStrategy = $namingStrategy; - $this->doctrineReader = $reader; - $this->mediaTypes = $mediaTypes; } /** @@ -67,7 +55,10 @@ public function __construct( public function describe(Model $model, OA\Schema $schema) { $className = $model->getType()->getClassName(); + + /** @var ?ClassMetadata $metadata */ $metadata = $this->factory->getMetadataForClass($className); + if (null === $metadata) { throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className)); } @@ -80,6 +71,8 @@ public function describe(Model $model, OA\Schema $schema) $context = $this->getSerializationContext($model); $context->pushClassMetadata($metadata); + + /** @var PropertyMetadata $item */ foreach ($metadata->propertyMetadata as $item) { // filter groups if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) { @@ -101,13 +94,13 @@ public function describe(Model $model, OA\Schema $schema) if (null !== $item->getter) { try { $reflections[] = new \ReflectionMethod($item->class, $item->getter); - } catch (\ReflectionException $ignored) { + } catch (\ReflectionException) { } } if (null !== $item->setter) { try { $reflections[] = new \ReflectionMethod($item->class, $item->setter); - } catch (\ReflectionException $ignored) { + } catch (\ReflectionException) { } } @@ -134,7 +127,7 @@ public function describe(Model $model, OA\Schema $schema) $annotationsReader->updateProperty($reflection, $property, $groups); } - if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) { + if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) { $context->popPropertyMetadata(); continue; @@ -180,7 +173,7 @@ public function getSerializationContext(Model $model): SerializationContext return $context; } - private function computeGroups(Context $context, array $type = null) + private function computeGroups(Context $context, array $type = null): ?array { if (null === $type || true !== $this->propertyTypeUsesGroups($type)) { return null; @@ -210,7 +203,7 @@ public function supports(Model $model): bool if ($this->factory->getMetadataForClass($className)) { return true; } - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { } return false; @@ -278,7 +271,7 @@ public function describeItem(array $type, OA\Schema $property, Context $context) } } - private function getNestedTypeInArray(array $type) + private function getNestedTypeInArray(array $type): ?array { if ('array' !== $type['name'] && 'ArrayCollection' !== $type['name']) { return null; @@ -295,10 +288,7 @@ private function getNestedTypeInArray(array $type) return null; } - /** - * @return bool|null - */ - private function propertyTypeUsesGroups(array $type) + private function propertyTypeUsesGroups(array $type): ?bool { if (array_key_exists($type['name'], $this->propertyTypeUseGroupsCache)) { return $this->propertyTypeUseGroupsCache[$type['name']]; @@ -317,7 +307,7 @@ private function propertyTypeUsesGroups(array $type) $this->propertyTypeUseGroupsCache[$type['name']] = false; return false; - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { $this->propertyTypeUseGroupsCache[$type['name']] = null; return null; diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php index 0aa339a75..36118d88f 100644 --- a/ModelDescriber/ObjectModelDescriber.php +++ b/ModelDescriber/ObjectModelDescriber.php @@ -18,8 +18,8 @@ use Nelmio\ApiDocBundle\Model\Model; use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader; use Nelmio\ApiDocBundle\OpenApiPhp\Util; -use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Annotation\DiscriminatorMap; @@ -30,31 +30,18 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar use ModelRegistryAwareTrait; use ApplyOpenApiDiscriminatorTrait; - /** @var PropertyInfoExtractorInterface */ - private $propertyInfo; - /** @var Reader */ - private $doctrineReader; - /** @var PropertyDescriberInterface[] */ - private $propertyDescribers; - /** @var string[] */ - private $mediaTypes; - /** @var NameConverterInterface[] */ - private $nameConverter; - public function __construct( - PropertyInfoExtractorInterface $propertyInfo, - Reader $reader, - iterable $propertyDescribers, - array $mediaTypes, - NameConverterInterface $nameConverter = null + private PropertyInfoExtractorInterface $propertyInfo, + private Reader $doctrineReader, + private iterable $propertyDescribers, + private array $mediaTypes, + private ?NameConverterInterface $nameConverter = null ) { - $this->propertyInfo = $propertyInfo; - $this->doctrineReader = $reader; - $this->propertyDescribers = $propertyDescribers; - $this->mediaTypes = $mediaTypes; - $this->nameConverter = $nameConverter; } + /** + * @throws \ReflectionException + */ public function describe(Model $model, OA\Schema $schema) { $schema->type = 'object'; @@ -72,7 +59,7 @@ public function describe(Model $model, OA\Schema $schema) $annotationsReader->updateDefinition($reflClass, $schema); $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class); - if ($discriminatorMap && OA\UNDEFINED === $schema->discriminator) { + if ($discriminatorMap && Generator::UNDEFINED === $schema->discriminator) { $this->applyOpenApiDiscriminator( $model, $schema, @@ -114,7 +101,7 @@ public function describe(Model $model, OA\Schema $schema) } // If type manually defined - if (OA\UNDEFINED !== $property->type || OA\UNDEFINED !== $property->ref) { + if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) { continue; } @@ -157,6 +144,8 @@ private function camelize(string $string): string /** * @param Type[] $types + * + * @throws \Exception */ private function describeProperty(array $types, Model $model, OA\Schema $property, string $propertyName) { diff --git a/OpenApiPhp/DefaultOperationId.php b/OpenApiPhp/DefaultOperationId.php index b923b8c86..0a30fce1b 100644 --- a/OpenApiPhp/DefaultOperationId.php +++ b/OpenApiPhp/DefaultOperationId.php @@ -13,6 +13,7 @@ use OpenApi\Analysis; use OpenApi\Annotations as OA; +use OpenApi\Generator; /** * Disable the OperationId processor from zircote/swagger-php as it breaks our documentation by setting non-unique operation ids. @@ -27,7 +28,7 @@ public function __invoke(Analysis $analysis) $allOperations = $analysis->getAnnotationsOfType(OA\Operation::class); foreach ($allOperations as $operation) { - if (OA\UNDEFINED === $operation->operationId) { + if (Generator::UNDEFINED === $operation->operationId) { $operation->operationId = null; } } diff --git a/OpenApiPhp/ModelRegister.php b/OpenApiPhp/ModelRegister.php index 16dd6d0c1..1f003c1f3 100644 --- a/OpenApiPhp/ModelRegister.php +++ b/OpenApiPhp/ModelRegister.php @@ -25,16 +25,8 @@ */ final class ModelRegister { - /** @var ModelRegistry */ - private $modelRegistry; - - /** @var string[] */ - private $mediaTypes; - - public function __construct(ModelRegistry $modelRegistry, array $mediaTypes) + public function __construct(private ModelRegistry $modelRegistry, private array $mediaTypes) { - $this->modelRegistry = $modelRegistry; - $this->mediaTypes = $mediaTypes; } public function __invoke(Analysis $analysis, array $parentGroups = null) @@ -54,7 +46,7 @@ public function __invoke(Analysis $analysis, array $parentGroups = null) // Misusage of ::$ref if (($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) && $annotation->ref instanceof ModelAnnotation) { - throw new \InvalidArgumentException(sprintf('Using @Model inside @%s::$ref is not allowed. You should use ::$ref with @Property, @Parameter, @Schema, @Items but within @Response or @RequestBody you should put @Model directly at the root of the annotation : `@Response(..., @Model(...))`.', get_class($annotation))); + throw new \InvalidArgumentException(sprintf('Using @Model inside @%s::$ref is not allowed. You should use ::$ref with @Property, @Parameter, @Schema, @Items but within @Response or @RequestBody you should put @Model directly at the root of the annotation : `@Response(..., @Model(...))`.', $annotation::class)); } // Implicit usages @@ -82,7 +74,7 @@ public function __invoke(Analysis $analysis, array $parentGroups = null) } if (!$annotation instanceof OA\Parameter) { - throw new \InvalidArgumentException(sprintf("@Model annotation can't be nested with an annotation of type @%s.", get_class($annotation))); + throw new \InvalidArgumentException(sprintf("@Model annotation can't be nested with an annotation of type @%s.", $annotation::class)); } if ($annotation->schema instanceof OA\Schema && 'array' === $annotation->schema->type) { @@ -128,7 +120,7 @@ private function detach(ModelAnnotation $model, OA\AbstractAnnotation $annotatio private function createType(string $type): Type { - if ('[]' === substr($type, -2)) { + if (str_ends_with($type, '[]')) { return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2))); } @@ -152,18 +144,11 @@ private function createContentForMediaType( OA\AbstractAnnotation $annotation, Analysis $analysis ) { - switch ($type) { - case 'json': - $modelAnnotation = new OA\JsonContent($properties); - - break; - case 'xml': - $modelAnnotation = new OA\XmlContent($properties); - - break; - default: - throw new \InvalidArgumentException(sprintf("@Model annotation is not compatible with the media types '%s'. It must be one of 'json' or 'xml'.", implode(',', $this->mediaTypes))); - } + $modelAnnotation = match ($type) { + 'json' => new OA\JsonContent($properties), + 'xml' => new OA\XmlContent($properties), + default => throw new \InvalidArgumentException(sprintf("@Model annotation is not compatible with the media types '%s'. It must be one of 'json' or 'xml'.", implode(',', $this->mediaTypes))), + }; $annotation->merge([$modelAnnotation]); $analysis->addAnnotation($modelAnnotation, $properties['_context']); diff --git a/OpenApiPhp/Util.php b/OpenApiPhp/Util.php index ba97edeb2..1c1100f31 100644 --- a/OpenApiPhp/Util.php +++ b/OpenApiPhp/Util.php @@ -13,7 +13,7 @@ use OpenApi\Annotations as OA; use OpenApi\Context; -use const OpenApi\UNDEFINED; +use OpenApi\Generator; /** * Class Util. @@ -46,7 +46,7 @@ * @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::createContext() * * The merge method @see \Nelmio\ApiDocBundle\OpenApiPhp\Util::merge() has the main purpose to be able - * to merge properties from an deeply nested array of Annotation properties in the structure of a + * to merge properties from a deeply nested array of Annotation properties in the structure of a * generated swagger json decoded array. */ final class Util @@ -62,12 +62,10 @@ final class Util * Return an existing PathItem object from $api->paths[] having its member path set to $path. * Create, add to $api->paths[] and return this new PathItem object and set the property if none found. * - * @see OA\OpenApi::$paths * @see OA\PathItem::path - * - * @param string $path + * @see OA\OpenApi::$paths */ - public static function getPath(OA\OpenApi $api, $path): OA\PathItem + public static function getPath(OA\OpenApi $api, string $path): OA\PathItem { return self::getIndexedCollectionItem($api, OA\PathItem::class, $path); } @@ -76,12 +74,10 @@ public static function getPath(OA\OpenApi $api, $path): OA\PathItem * Return an existing Schema object from $api->components->schemas[] having its member schema set to $schema. * Create, add to $api->components->schemas[] and return this new Schema object and set the property if none found. * - * @param string $schema - * * @see OA\Schema::$schema * @see OA\Components::$schemas */ - public static function getSchema(OA\OpenApi $api, $schema): OA\Schema + public static function getSchema(OA\OpenApi $api, string $schema): OA\Schema { if (!$api->components instanceof OA\Components) { $api->components = new OA\Components([]); @@ -97,12 +93,10 @@ public static function getSchema(OA\OpenApi $api, $schema): OA\Schema * Create, add to $schema->properties[] and return this new Property object * and set the property if none found. * - * @see OA\Schema::$properties * @see OA\Property::$property - * - * @param string $property + * @see OA\Schema::$properties */ - public static function getProperty(OA\Schema $schema, $property): OA\Property + public static function getProperty(OA\Schema $schema, string $property): OA\Property { return self::getIndexedCollectionItem($schema, OA\Property::class, $property); } @@ -111,17 +105,15 @@ public static function getProperty(OA\Schema $schema, $property): OA\Property * Return an existing Operation from $path->{$method} * or create, set $path->{$method} and return this new Operation object. * - * @see OA\PathItem::$get * @see OA\PathItem::$post * @see OA\PathItem::$put * @see OA\PathItem::$patch * @see OA\PathItem::$delete * @see OA\PathItem::$options * @see OA\PathItem::$head - * - * @param string $method + * @see OA\PathItem::$get */ - public static function getOperation(OA\PathItem $path, $method): OA\Operation + public static function getOperation(OA\PathItem $path, string $method): OA\Operation { $class = array_keys($path::$_nested, \strtolower($method), true)[0]; @@ -138,11 +130,8 @@ public static function getOperation(OA\PathItem $path, $method): OA\Operation * @see OA\Operation::$parameters * @see OA\Parameter::$name * @see OA\Parameter::$in - * - * @param string $name - * @param string $in */ - public static function getOperationParameter(OA\Operation $operation, $name, $in): OA\Parameter + public static function getOperationParameter(OA\Operation $operation, string $name, string $in): OA\Parameter { return self::getCollectionItem($operation, OA\Parameter::class, ['name' => $name, 'in' => $in]); } @@ -155,15 +144,16 @@ public static function getOperationParameter(OA\Operation $operation, $name, $in * it is expected to be a string nested property. * * @see OA\AbstractAnnotation::$_nested - * - * @param $class */ - public static function getChild(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation - { + public static function getChild( + OA\AbstractAnnotation $parent, + string $class, + array $properties = [] + ): OA\AbstractAnnotation { $nested = $parent::$_nested; $property = $nested[$class]; - if (null === $parent->{$property} || UNDEFINED === $parent->{$property}) { + if (null === $parent->{$property} || Generator::UNDEFINED === $parent->{$property}) { $parent->{$property} = self::createChild($parent, $class, $properties); } @@ -181,18 +171,19 @@ public static function getChild(OA\AbstractAnnotation $parent, $class, array $pr * it is expected to be a single value array nested Annotation. * * @see OA\AbstractAnnotation::$_nested - * - * @param string $class */ - public static function getCollectionItem(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation - { + public static function getCollectionItem( + OA\AbstractAnnotation $parent, + string $class, + array $properties = [] + ): OA\AbstractAnnotation { $key = null; $nested = $parent::$_nested; $collection = $nested[$class][0]; if (!empty($properties)) { $key = self::searchCollectionItem( - $parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [], + $parent->{$collection} && Generator::UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [], $properties ); } @@ -214,17 +205,17 @@ public static function getCollectionItem(OA\AbstractAnnotation $parent, $class, * with the second value being the mapping index $property. * * @see OA\AbstractAnnotation::$_nested - * - * @param string $class - * @param mixed $value */ - public static function getIndexedCollectionItem(OA\AbstractAnnotation $parent, $class, $value): OA\AbstractAnnotation - { + public static function getIndexedCollectionItem( + OA\AbstractAnnotation $parent, + string $class, + mixed $value + ): OA\AbstractAnnotation { $nested = $parent::$_nested; [$collection, $property] = $nested[$class]; $key = self::searchIndexedCollectionItem( - $parent->{$collection} && UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [], + $parent->{$collection} && Generator::UNDEFINED !== $parent->{$collection} ? $parent->{$collection} : [], $property, $value ); @@ -239,10 +230,8 @@ public static function getIndexedCollectionItem(OA\AbstractAnnotation $parent, $ /** * Search for an Annotation within $collection that has all members set * to the respective values in the associative array $properties. - * - * @return int|string|null */ - public static function searchCollectionItem(array $collection, array $properties) + public static function searchCollectionItem(array $collection, array $properties): int|string|null { foreach ($collection ?: [] as $i => $child) { foreach ($properties as $k => $prop) { @@ -259,13 +248,8 @@ public static function searchCollectionItem(array $collection, array $properties /** * Search for an Annotation within the $collection that has its member $index set to $value. - * - * @param string $member - * @param mixed $value - * - * @return false|int|string */ - public static function searchIndexedCollectionItem(array $collection, $member, $value) + public static function searchIndexedCollectionItem(array $collection, string $member, mixed $value): int|string|false { return array_search($value, array_column($collection, $member), true); } @@ -273,13 +257,14 @@ public static function searchIndexedCollectionItem(array $collection, $member, $ /** * Create a new Object of $class with members $properties within $parent->{$collection}[] * and return the created index. - * - * @param string $collection - * @param string $class */ - public static function createCollectionItem(OA\AbstractAnnotation $parent, $collection, $class, array $properties = []): int - { - if (UNDEFINED === $parent->{$collection}) { + public static function createCollectionItem( + OA\AbstractAnnotation $parent, + string $collection, + string $class, + array $properties = [] + ): int { + if (Generator::UNDEFINED === $parent->{$collection}) { $parent->{$collection} = []; } @@ -292,12 +277,13 @@ public static function createCollectionItem(OA\AbstractAnnotation $parent, $coll /** * Create a new Object of $class with members $properties and set the context parent to be $parent. * - * @param string $class - * * @throws \InvalidArgumentException at an attempt to pass in properties that are found in $parent::$_nested */ - public static function createChild(OA\AbstractAnnotation $parent, $class, array $properties = []): OA\AbstractAnnotation - { + public static function createChild( + OA\AbstractAnnotation $parent, + string $class, + array $properties = [] + ): OA\AbstractAnnotation { $nesting = self::getNestingIndexes($class); if (!empty(array_intersect(array_keys($properties), $nesting))) { @@ -316,8 +302,6 @@ public static function createChild(OA\AbstractAnnotation $parent, $class, array */ public static function createContext(array $properties = [], Context $parent = null): Context { - $properties['comment'] = ''; // TODO: remove this when https://github.com/zircote/swagger-php/commit/708a25208797ca05ebeae572bbccad8b13de14d8 is released - return new Context($properties, $parent); } @@ -326,11 +310,12 @@ public static function createContext(array $properties = [], Context $parent = n * * The main purpose is to create a Swagger Object from array config values * in the structure of a json serialized Swagger object. - * - * @param array|\ArrayObject|OA\AbstractAnnotation $from */ - public static function merge(OA\AbstractAnnotation $annotation, $from, bool $overwrite = false) - { + public static function merge( + OA\AbstractAnnotation $annotation, + array|\ArrayObject|OA\AbstractAnnotation $from, + bool $overwrite = false + ) { if (\is_array($from)) { self::mergeFromArray($annotation, $from, $overwrite); } elseif (\is_a($from, OA\AbstractAnnotation::class)) { @@ -346,7 +331,7 @@ private static function mergeFromArray(OA\AbstractAnnotation $annotation, array { $done = []; - $defaults = \get_class_vars(\get_class($annotation)); + $defaults = \get_class_vars($annotation::class); foreach ($annotation::$_nested as $className => $propertyName) { if (\is_string($propertyName)) { @@ -362,7 +347,7 @@ private static function mergeFromArray(OA\AbstractAnnotation $annotation, array } elseif (\array_key_exists($propertyName[0], $properties)) { $collection = $propertyName[0]; $property = $propertyName[1] ?? null; - self::mergeCollection($annotation, $className, $collection, $property, $properties[$collection], $overwrite); + self::mergeCollection($annotation, $className, $property, $properties[$collection], $overwrite); $done[] = $collection; } } @@ -389,7 +374,7 @@ private static function mergeChild(OA\AbstractAnnotation $annotation, $className self::merge(self::getChild($annotation, $className), $value, $overwrite); } - private static function mergeCollection(OA\AbstractAnnotation $annotation, $className, $collection, $property, $items, bool $overwrite) + private static function mergeCollection(OA\AbstractAnnotation $annotation, $className, $property, $items, bool $overwrite) { if (null !== $property) { foreach ($items as $prop => $value) { @@ -415,10 +400,10 @@ private static function mergeCollection(OA\AbstractAnnotation $annotation, $clas private static function mergeTyped(OA\AbstractAnnotation $annotation, $propertyName, $type, array $properties, array $defaults, bool $overwrite) { - if (\is_string($type) && 0 === strpos($type, '[')) { + if (\is_string($type) && str_starts_with($type, '[')) { $innerType = substr($type, 1, -1); - if (!$annotation->{$propertyName} || UNDEFINED === $annotation->{$propertyName}) { + if (!$annotation->{$propertyName} || Generator::UNDEFINED === $annotation->{$propertyName}) { $annotation->{$propertyName} = []; } diff --git a/PropertyDescriber/ArrayPropertyDescriber.php b/PropertyDescriber/ArrayPropertyDescriber.php index ee89c1728..8ecac527a 100644 --- a/PropertyDescriber/ArrayPropertyDescriber.php +++ b/PropertyDescriber/ArrayPropertyDescriber.php @@ -22,20 +22,17 @@ class ArrayPropertyDescriber implements PropertyDescriberInterface, ModelRegistr use ModelRegistryAwareTrait; use NullablePropertyTrait; - /** @var PropertyDescriberInterface[] */ - private $propertyDescribers; - - public function __construct(iterable $propertyDescribers = []) + /** + * @param PropertyDescriberInterface[] $propertyDescribers + */ + public function __construct(private iterable $propertyDescribers = []) { - $this->propertyDescribers = $propertyDescribers; } public function describe(array $types, OA\Schema $property, array $groups = null) { - // BC layer for symfony < 5.3 - $type = method_exists($types[0], 'getCollectionValueTypes') ? - ($types[0]->getCollectionValueTypes()[0] ?? null) : - $types[0]->getCollectionValueType(); + $type = $types[0]->getCollectionValueTypes()[0] ?? null; + if (null === $type) { throw new UndocumentedArrayItemsException(); } diff --git a/PropertyDescriber/CompoundPropertyDescriber.php b/PropertyDescriber/CompoundPropertyDescriber.php index ba6cf6c27..a06df12e3 100644 --- a/PropertyDescriber/CompoundPropertyDescriber.php +++ b/PropertyDescriber/CompoundPropertyDescriber.php @@ -15,22 +15,19 @@ use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; class CompoundPropertyDescriber implements PropertyDescriberInterface, ModelRegistryAwareInterface { use ModelRegistryAwareTrait; - /** @var PropertyDescriberInterface[] */ - private $propertyDescribers; - - public function __construct(iterable $propertyDescribers) + public function __construct(private iterable $propertyDescribers) { - $this->propertyDescribers = $propertyDescribers; } public function describe(array $types, OA\Schema $property, array $groups = null) { - $property->oneOf = OA\UNDEFINED !== $property->oneOf ? $property->oneOf : []; + $property->oneOf = Generator::UNDEFINED !== $property->oneOf ? $property->oneOf : []; foreach ($types as $type) { $property->oneOf[] = $schema = Util::createChild($property, OA\Schema::class, []); diff --git a/PropertyDescriber/ObjectPropertyDescriber.php b/PropertyDescriber/ObjectPropertyDescriber.php index ef1aabc4d..d29e3f91f 100644 --- a/PropertyDescriber/ObjectPropertyDescriber.php +++ b/PropertyDescriber/ObjectPropertyDescriber.php @@ -28,11 +28,8 @@ public function describe(array $types, OA\Schema $property, array $groups = null false, $types[0]->getClassName(), $types[0]->isCollection(), - $types[0]->getCollectionKeyType(), - // BC layer for symfony < 5.3 - method_exists($types[0], 'getCollectionValueTypes') ? - ($types[0]->getCollectionValueTypes()[0] ?? null) : - $types[0]->getCollectionValueType() + $types[0]->getCollectionKeyTypes(), + $types[0]->getCollectionValueTypes()[0] ?? null ); // ignore nullable field if ($types[0]->isNullable()) { diff --git a/Render/Html/GetNelmioAsset.php b/Render/Html/GetNelmioAsset.php index be642830c..dd4c220ba 100644 --- a/Render/Html/GetNelmioAsset.php +++ b/Render/Html/GetNelmioAsset.php @@ -20,18 +20,16 @@ */ class GetNelmioAsset extends AbstractExtension { - private $assetExtension; - private $resourcesDir; - private $cdnUrl; + private string $resourcesDir; + private string $cdnUrl; - public function __construct(AssetExtension $assetExtension) + public function __construct(private AssetExtension $assetExtension) { - $this->assetExtension = $assetExtension; $this->cdnUrl = 'https://cdn.jsdelivr.net/gh/nelmio/NelmioApiDocBundle/Resources/public'; $this->resourcesDir = __DIR__.'/../../Resources/public'; } - public function getFunctions() + public function getFunctions(): array { return [ new TwigFunction('nelmioAsset', $this, ['is_safe' => ['html']]), @@ -51,7 +49,7 @@ public function __invoke($defaultAssetsMode, $asset) } } - private function getExtension($assetsMode, $asset) + private function getExtension($assetsMode, $asset): array { $extension = mb_substr($asset, -3, 3, 'utf-8'); if ('.js' === $extension) { @@ -63,7 +61,7 @@ private function getExtension($assetsMode, $asset) } } - private function getResource($asset, $mode) + private function getResource($asset, $mode): array { if (filter_var($asset, FILTER_VALIDATE_URL)) { return [$asset, false]; @@ -76,7 +74,7 @@ private function getResource($asset, $mode) } } - private function renderJavascript(string $script, bool $isInline) + private function renderJavascript(string $script, bool $isInline): string { if ($isInline) { return sprintf('', $script); @@ -85,7 +83,7 @@ private function renderJavascript(string $script, bool $isInline) } } - private function renderCss(string $stylesheet, bool $isInline) + private function renderCss(string $stylesheet, bool $isInline): string { if ($isInline) { return sprintf('', $stylesheet); diff --git a/Render/Html/HtmlOpenApiRenderer.php b/Render/Html/HtmlOpenApiRenderer.php index 39d844cd7..3b380aa91 100644 --- a/Render/Html/HtmlOpenApiRenderer.php +++ b/Render/Html/HtmlOpenApiRenderer.php @@ -16,22 +16,21 @@ use Nelmio\ApiDocBundle\Render\RenderOpenApi; use OpenApi\Annotations\OpenApi; use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; /** * @internal */ class HtmlOpenApiRenderer implements OpenApiRenderer { - /** @var Environment|\Twig_Environment */ - private $twig; - - /** @var GetNelmioAsset */ - private $getNelmioAsset; + private \Twig_Environment|Environment $twig; public function __construct($twig) { if (!$twig instanceof \Twig_Environment && !$twig instanceof Environment) { - throw new InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', get_class($twig))); + throw new InvalidArgumentException(sprintf('Providing an instance of "%s" as twig is not supported.', $twig::class)); } $this->twig = $twig; } @@ -41,6 +40,11 @@ public function getFormat(): string return RenderOpenApi::HTML; } + /** + * @throws RuntimeError + * @throws SyntaxError + * @throws LoaderError + */ public function render(OpenApi $spec, array $options = []): string { $options += [ diff --git a/Render/RenderOpenApi.php b/Render/RenderOpenApi.php index c9b759469..49eeac9e5 100644 --- a/Render/RenderOpenApi.php +++ b/Render/RenderOpenApi.php @@ -23,15 +23,11 @@ class RenderOpenApi public const JSON = 'json'; public const YAML = 'yaml'; - /** @var ContainerInterface */ - private $generatorLocator; - /** @var array */ - private $openApiRenderers = []; + private array $openApiRenderers = []; - public function __construct(ContainerInterface $generatorLocator, ?OpenApiRenderer ...$openApiRenderers) + public function __construct(private ContainerInterface $generatorLocator, ?OpenApiRenderer ...$openApiRenderers) { - $this->generatorLocator = $generatorLocator; foreach ($openApiRenderers as $openApiRenderer) { if (null === $openApiRenderer) { continue; @@ -46,7 +42,7 @@ public function getAvailableFormats(): array return array_keys($this->openApiRenderers); } - public function renderFromRequest(Request $request, string $format, $area, array $extraOptions = []) + public function renderFromRequest(Request $request, string $format, $area, array $extraOptions = []): string { $options = []; if ('' !== $request->getBaseUrl()) { diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 9daa3ea0d..95e294485 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -39,7 +39,7 @@ Open a command console, enter your project directory and execute the following c class AppKernel extends Kernel { - public function registerBundles() + public function registerBundles(): iterable { $bundles = [ // ... diff --git a/Resources/views/SwaggerUi/index.html.twig b/Resources/views/SwaggerUi/index.html.twig index a711c2571..fac6ea3e7 100644 --- a/Resources/views/SwaggerUi/index.html.twig +++ b/Resources/views/SwaggerUi/index.html.twig @@ -51,13 +51,16 @@ file that was distributed with this source code. #} {% endblock svg_icons %} -
- {% block header %} - - {% endblock header %} -
+ + {% block header_block %} +
+ {% block header %} + + {% endblock header %} +
+ {% endblock header_block %} {% block swagger_ui %}
diff --git a/RouteDescriber/FosRestDescriber.php b/RouteDescriber/FosRestDescriber.php index e3f9f4a4c..458db62d5 100644 --- a/RouteDescriber/FosRestDescriber.php +++ b/RouteDescriber/FosRestDescriber.php @@ -11,11 +11,13 @@ namespace Nelmio\ApiDocBundle\RouteDescriber; +use DateTimeInterface; use Doctrine\Common\Annotations\Reader; use FOS\RestBundle\Controller\Annotations\QueryParam; use FOS\RestBundle\Controller\Annotations\RequestParam; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\Routing\Route; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\DateTime; @@ -25,16 +27,8 @@ final class FosRestDescriber implements RouteDescriberInterface { use RouteDescriberTrait; - /** @var Reader */ - private $annotationReader; - - /** @var string[] */ - private $mediaTypes; - - public function __construct(Reader $annotationReader, array $mediaTypes) + public function __construct(private Reader $annotationReader, private array $mediaTypes) { - $this->annotationReader = $annotationReader; - $this->mediaTypes = $mediaTypes; } public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod) @@ -55,7 +49,7 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle $parameter->required = !$annotation->nullable && $annotation->strict; - if (OA\UNDEFINED === $parameter->description) { + if (Generator::UNDEFINED === $parameter->description) { $parameter->description = $annotation->description; } @@ -63,6 +57,7 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle $parameter->explode = true; } + /** @var OA\Schema $schema */ $schema = Util::getChild($parameter, OA\Schema::class); $this->describeCommonSchemaFromAnnotation($schema, $annotation); } else { @@ -85,7 +80,7 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle } } - private function getPattern($requirements) + private function getPattern($requirements): ?string { if (is_array($requirements) && isset($requirements['rule'])) { return (string) $requirements['rule']; @@ -102,12 +97,12 @@ private function getPattern($requirements) return null; } - private function getFormat($requirements) + private function getFormat($requirements): ?string { if ($requirements instanceof Constraint && !$requirements instanceof Regex) { if ($requirements instanceof DateTime) { // As defined per RFC3339 - if (\DateTime::RFC3339 === $requirements->format || 'c' === $requirements->format) { + if (DateTimeInterface::RFC3339 === $requirements->format || 'c' === $requirements->format) { return 'date-time'; } @@ -128,19 +123,12 @@ private function getFormat($requirements) private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema { - $requestBody->content = OA\UNDEFINED !== $requestBody->content ? $requestBody->content : []; - switch ($type) { - case 'json': - $contentType = 'application/json'; - - break; - case 'xml': - $contentType = 'application/xml'; - - break; - default: - throw new \InvalidArgumentException('Unsupported media type'); - } + $requestBody->content = Generator::UNDEFINED !== $requestBody->content ? $requestBody->content : []; + $contentType = match ($type) { + 'json' => 'application/json', + 'xml' => 'application/xml', + default => throw new \InvalidArgumentException('Unsupported media type'), + }; if (!isset($requestBody->content[$contentType])) { $requestBody->content[$contentType] = new OA\MediaType( [ @@ -165,7 +153,7 @@ private function describeCommonSchemaFromAnnotation(OA\Schema $schema, $annotati { $schema->default = $annotation->getDefault(); - if (OA\UNDEFINED === $schema->type) { + if (Generator::UNDEFINED === $schema->type) { $schema->type = $annotation->map ? 'array' : 'string'; } diff --git a/RouteDescriber/PhpDocDescriber.php b/RouteDescriber/PhpDocDescriber.php index 1ae2ef712..ed1c4c730 100644 --- a/RouteDescriber/PhpDocDescriber.php +++ b/RouteDescriber/PhpDocDescriber.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\RouteDescriber; use OpenApi\Annotations as OA; +use OpenApi\Generator; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactoryInterface; use Symfony\Component\Routing\Route; @@ -20,7 +21,7 @@ final class PhpDocDescriber implements RouteDescriberInterface { use RouteDescriberTrait; - private $docBlockFactory; + private DocBlockFactory|null|DocBlockFactoryInterface $docBlockFactory; public function __construct(DocBlockFactoryInterface $docBlockFactory = null) { @@ -37,20 +38,20 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle try { $classDocBlock = $this->docBlockFactory->create($reflectionMethod->getDeclaringClass()); - } catch (\Exception $e) { + } catch (\Exception) { } try { $docBlock = $this->docBlockFactory->create($reflectionMethod); - } catch (\Exception $e) { + } catch (\Exception) { } foreach ($this->getOperations($api, $route) as $operation) { if (null !== $docBlock) { - if (OA\UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) { + if (Generator::UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) { $operation->summary = $docBlock->getSummary(); } - if (OA\UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) { + if (Generator::UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) { $operation->description = (string) $docBlock->getDescription(); } if ($docBlock->hasTag('deprecated')) { diff --git a/RouteDescriber/RouteDescriberTrait.php b/RouteDescriber/RouteDescriberTrait.php index 7788a5e45..b089c8913 100644 --- a/RouteDescriber/RouteDescriberTrait.php +++ b/RouteDescriber/RouteDescriberTrait.php @@ -42,7 +42,7 @@ private function getOperations(OpenApi $api, Route $route): array private function normalizePath(string $path): string { - if ('.{_format}' === substr($path, -10)) { + if (str_ends_with($path, '.{_format}')) { $path = substr($path, 0, -10); } diff --git a/RouteDescriber/RouteMetadataDescriber.php b/RouteDescriber/RouteMetadataDescriber.php index f68daddb3..eb0796985 100644 --- a/RouteDescriber/RouteMetadataDescriber.php +++ b/RouteDescriber/RouteMetadataDescriber.php @@ -14,6 +14,7 @@ use LogicException; use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\Routing\Route; /** @@ -40,7 +41,7 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle /** @var OA\Parameter $parameter */ $parameter = $existingParams[$paramId] ?? null; if (null !== $parameter) { - if (!$parameter->required || OA\UNDEFINED === $parameter->required) { + if (!$parameter->required || Generator::UNDEFINED === $parameter->required) { throw new LogicException(\sprintf('Global parameter "%s" is used as part of route "%s" and must be set as "required"', $pathVariable, $route->getPath())); } @@ -52,11 +53,11 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle $parameter->schema = Util::getChild($parameter, OA\Schema::class); - if (OA\UNDEFINED === $parameter->schema->type) { + if (Generator::UNDEFINED === $parameter->schema->type) { $parameter->schema->type = 'string'; } - if (isset($requirements[$pathVariable]) && OA\UNDEFINED === $parameter->schema->pattern) { + if (isset($requirements[$pathVariable]) && Generator::UNDEFINED === $parameter->schema->pattern) { $parameter->schema->pattern = $requirements[$pathVariable]; } } @@ -71,15 +72,15 @@ public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $refle private function getRefParams(OA\OpenApi $api, OA\Operation $operation): array { /** @var OA\Parameter[] $globalParams */ - $globalParams = OA\UNDEFINED !== $api->components && OA\UNDEFINED !== $api->components->parameters ? $api->components->parameters : []; + $globalParams = Generator::UNDEFINED !== $api->components && Generator::UNDEFINED !== $api->components->parameters ? $api->components->parameters : []; $globalParams = array_column($globalParams, null, 'parameter'); // update the indexes of the array with the reference names actually used $existingParams = []; - $operationParameters = OA\UNDEFINED !== $operation->parameters ? $operation->parameters : []; + $operationParameters = Generator::UNDEFINED !== $operation->parameters ? $operation->parameters : []; /** @var OA\Parameter $parameter */ - foreach ($operationParameters as $id => $parameter) { + foreach ($operationParameters as $parameter) { $ref = $parameter->ref; - if (OA\UNDEFINED === $ref) { + if (Generator::UNDEFINED === $ref) { // we only concern ourselves with '$ref' parameters, so continue the loop continue; } diff --git a/Routing/FilteredRouteCollectionBuilder.php b/Routing/FilteredRouteCollectionBuilder.php index 4c9b14b70..b73ee944b 100644 --- a/Routing/FilteredRouteCollectionBuilder.php +++ b/Routing/FilteredRouteCollectionBuilder.php @@ -20,23 +20,11 @@ final class FilteredRouteCollectionBuilder { - /** @var Reader */ - private $annotationReader; - - /** @var ControllerReflector */ - private $controllerReflector; - - /** @var string */ - private $area; - - /** @var array */ - private $options; - public function __construct( - Reader $annotationReader, - ControllerReflector $controllerReflector, - string $area, - array $options = [] + private Reader $annotationReader, + private ControllerReflector $controllerReflector, + private string $area, + private array $options = [] ) { $resolver = new OptionsResolver(); $resolver @@ -60,10 +48,6 @@ public function __construct( $normalizedOptions = ['path_patterns' => $options]; $options = $normalizedOptions; } - - $this->annotationReader = $annotationReader; - $this->controllerReflector = $controllerReflector; - $this->area = $area; $this->options = $resolver->resolve($options); } @@ -159,8 +143,8 @@ private function defaultRouteDisabled(Route $route): bool $annotations = $this->annotationReader->getMethodAnnotations($method); foreach ($annotations as $annotation) { - if (false !== strpos(get_class($annotation), 'Nelmio\\ApiDocBundle\\Annotation') - || false !== strpos(get_class($annotation), 'OpenApi\\Annotations') + if (str_contains($annotation::class, 'Nelmio\\ApiDocBundle\\Annotation') + || str_contains($annotation::class, 'OpenApi\\Annotations') ) { return true; } diff --git a/Tests/ApiDocGeneratorTest.php b/Tests/ApiDocGeneratorTest.php index ec51d4ca0..1642f7365 100644 --- a/Tests/ApiDocGeneratorTest.php +++ b/Tests/ApiDocGeneratorTest.php @@ -14,11 +14,15 @@ use Nelmio\ApiDocBundle\ApiDocGenerator; use Nelmio\ApiDocBundle\Describer\DefaultDescriber; use PHPUnit\Framework\TestCase; +use Psr\Cache\InvalidArgumentException; use Symfony\Component\Cache\Adapter\ArrayAdapter; class ApiDocGeneratorTest extends TestCase { - public function testCache() + /** + * @throws InvalidArgumentException + */ + public function testCache(): void { $adapter = new ArrayAdapter(); $generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter); @@ -26,7 +30,10 @@ public function testCache() $this->assertEquals(json_encode($generator->generate()), json_encode($adapter->getItem('openapi_doc')->get())); } - public function testCacheWithCustomId() + /** + * @throws InvalidArgumentException + */ + public function testCacheWithCustomId(): void { $adapter = new ArrayAdapter(); $generator = new ApiDocGenerator([new DefaultDescriber()], [], $adapter, 'custom_id'); diff --git a/Tests/Command/DumpCommandTest.php b/Tests/Command/DumpCommandTest.php index 9d82f386b..28a0a41b9 100644 --- a/Tests/Command/DumpCommandTest.php +++ b/Tests/Command/DumpCommandTest.php @@ -30,7 +30,7 @@ public function testJson(array $jsonOptions, int $expectedJsonFlags) ); } - public function provideJsonMode() + public function provideJsonMode(): array { return [ 'pretty print' => [[], JSON_PRETTY_PRINT], @@ -64,7 +64,7 @@ public function testHtml($htmlConfig, string $expectedHtml) self::assertStringContainsString($expectedHtml, $output); } - public function provideAssetsMode() + public function provideAssetsMode(): array { return [ 'default mode is cdn' => [ @@ -100,7 +100,7 @@ public function provideAssetsMode() ]; } - private function executeDumpCommand(array $options) + private function executeDumpCommand(array $options): string { $kernel = static::bootKernel(); $application = new Application($kernel); diff --git a/Tests/Describer/AbstractDescriberTest.php b/Tests/Describer/AbstractDescriberTest.php index 87090424f..0583a2e54 100644 --- a/Tests/Describer/AbstractDescriberTest.php +++ b/Tests/Describer/AbstractDescriberTest.php @@ -17,8 +17,7 @@ abstract class AbstractDescriberTest extends TestCase { - /** @var DescriberInterface */ - protected $describer; + protected DescriberInterface $describer; protected function getOpenApiDoc(): OpenApi { diff --git a/Tests/Describer/ApiPlatformDescriberTest.php b/Tests/Describer/ApiPlatformDescriberTest.php index 13fb1a552..5857351ad 100644 --- a/Tests/Describer/ApiPlatformDescriberTest.php +++ b/Tests/Describer/ApiPlatformDescriberTest.php @@ -19,9 +19,8 @@ class ApiPlatformDescriberTest extends AbstractDescriberTest { - private $documentation; - - private $normalizer; + private Documentation $documentation; + private NormalizerInterface $normalizer; public function testDescribe() { diff --git a/Tests/Describer/RouteDescriberTest.php b/Tests/Describer/RouteDescriberTest.php index 1d0c9ca32..c39c4b9d0 100644 --- a/Tests/Describer/RouteDescriberTest.php +++ b/Tests/Describer/RouteDescriberTest.php @@ -22,9 +22,8 @@ class RouteDescriberTest extends AbstractDescriberTest { - private $routes; - - private $routeDescriber; + private RouteCollection $routes; + private RouteDescriberInterface $routeDescriber; public function testIgnoreWhenNoController() { diff --git a/Tests/Functional/ArrayItemsErrorTest.php b/Tests/Functional/ArrayItemsErrorTest.php index 57a0ea01b..cfeaa6d4d 100644 --- a/Tests/Functional/ArrayItemsErrorTest.php +++ b/Tests/Functional/ArrayItemsErrorTest.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException; +use Symfony\Component\HttpKernel\KernelInterface; class ArrayItemsErrorTest extends WebTestCase { @@ -30,7 +31,7 @@ public function testModelPictureDocumentation() $this->getOpenApiDefinition(); } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return new TestKernel(TestKernel::ERROR_ARRAY_ITEMS); } diff --git a/Tests/Functional/BazingaFunctionalTest.php b/Tests/Functional/BazingaFunctionalTest.php index ff46e8dd9..eaa6f7804 100644 --- a/Tests/Functional/BazingaFunctionalTest.php +++ b/Tests/Functional/BazingaFunctionalTest.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use Hateoas\Configuration\Embedded; +use Symfony\Component\HttpKernel\KernelInterface; class BazingaFunctionalTest extends WebTestCase { @@ -98,7 +99,7 @@ public function testWithType() { try { new \ReflectionMethod(Embedded::class, 'getType'); - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { $this->markTestSkipped('Typed embedded properties require at least willdurand/hateoas 3.0'); } $this->assertEquals([ @@ -123,7 +124,7 @@ public function testWithType() ], json_decode($this->getModel('BazingaUserTyped')->toJson(), true)); } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return new TestKernel(TestKernel::USE_JMS | TestKernel::USE_BAZINGA); } diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 69372d372..e579979f6 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -25,9 +25,7 @@ use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route("/api", name="api_", host="api.example.com") - */ +#[Route(path: '/api', name: 'api_', host: 'api.example.com')] class ApiController { /** @@ -50,8 +48,6 @@ public function fetchArticleAction() /** * The method LINK is not supported by OpenAPI so the method will be ignored. * - * @Route("/swagger", methods={"GET", "LINK"}) - * @Route("/swagger2", methods={"GET"}) * @Operation( * @OA\Response(response="201", description="An example resource") * ) @@ -64,12 +60,13 @@ public function fetchArticleAction() * @OA\Response(response="203", description="but 203 is not actually allowed (wrong method)") * ) */ + #[Route(path: '/swagger', methods: ['GET', 'LINK'])] + #[Route(path: '/swagger2', methods: ['GET'])] public function swaggerAction() { } /** - * @Route("/swagger/implicit", methods={"GET", "POST"}) * @OA\Response( * response="201", * description="Operation automatically detected", @@ -84,12 +81,12 @@ public function swaggerAction() * ) * @OA\Tag(name="implicit") */ + #[Route(path: '/swagger/implicit', methods: ['GET', 'POST'])] public function implicitSwaggerAction() { } /** - * @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"}) * @OA\Response( * response="201", * description="Operation automatically detected", @@ -100,14 +97,15 @@ public function implicitSwaggerAction() * @Model(type=UserType::class, options={"bar": "baz"})) * ) */ + #[Route(path: '/test/users/{user}', methods: ['POST'], schemes: ['https'], requirements: ['user' => '/foo/'])] public function submitUserTypeAction() { } /** - * @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"}) * @OA\Response(response=200, description="sucessful") */ + #[Route(path: '/test/{user}', methods: ['GET'], schemes: ['https'], requirements: ['user' => '/foo/'])] public function userAction() { } @@ -117,19 +115,17 @@ public function userAction() * * Please do not use this action. * - * @Route("/deprecated", methods={"GET"}) - * * @deprecated */ + #[Route(path: '/deprecated', methods: ['GET'])] public function deprecatedAction() { } /** * This action is not documented. It is excluded by the config. - * - * @Route("/admin", methods={"GET"}) */ + #[Route(path: '/admin', methods: ['GET'])] public function adminAction() { } @@ -145,36 +141,36 @@ public function filteredAction() } /** - * @Route("/form", methods={"POST"}) * @OA\RequestBody( * description="Request content", * @Model(type=DummyType::class)) * ) * @OA\Response(response="201", description="") */ + #[Route(path: '/form', methods: ['POST'])] public function formAction() { } /** - * @Route("/security") * @OA\Response(response="201", description="") * @Security(name="api_key") * @Security(name="basic") * @Security(name="oauth2", scopes={"scope_1"}) */ + #[Route(path: '/security')] public function securityAction() { } /** - * @Route("/swagger/symfonyConstraints", methods={"GET"}) * @OA\Response( * response="201", * description="Used for symfony constraints test", * @Model(type=SymfonyConstraints::class) * ) */ + #[Route(path: '/swagger/symfonyConstraints', methods: ['GET'])] public function symfonyConstraintsAction() { } @@ -189,65 +185,59 @@ public function symfonyConstraintsAction() * response="201", * ref="#/components/responses/201" * ) - * @Route("/configReference", methods={"GET"}) */ + #[Route(path: '/configReference', methods: ['GET'])] public function configReferenceAction() { } /** - * @Route("/multi-annotations", methods={"GET", "POST"}) * @OA\Get(description="This is the get operation") * @OA\Post(description="This is post") - * * @OA\Response(response=200, description="Worked well!", @Model(type=DummyType::class)) */ + #[Route(path: '/multi-annotations', methods: ['GET', 'POST'])] public function operationsWithOtherAnnotations() { } /** - * @Route("/areas/new", methods={"GET", "POST"}) - * * @Areas({"area", "area2"}) */ + #[Route(path: '/areas/new', methods: ['GET', 'POST'])] public function newAreaAction() { } /** - * @Route("/compound", methods={"GET", "POST"}) - * * @OA\Response(response=200, description="Worked well!", @Model(type=CompoundEntity::class)) */ + #[Route(path: '/compound', methods: ['GET', 'POST'])] public function compoundEntityAction() { } /** - * @Route("/discriminator-mapping", methods={"GET", "POST"}) - * * @OA\Response(response=200, description="Worked well!", @Model(type=SymfonyDiscriminator::class)) */ + #[Route(path: '/discriminator-mapping', methods: ['GET', 'POST'])] public function discriminatorMappingAction() { } /** - * @Route("/named_route-operation-id", name="named_route_operation_id", methods={"GET", "POST"}) - * * @OA\Response(response=200, description="success") */ + #[Route(path: '/named_route-operation-id', name: 'named_route_operation_id', methods: ['GET', 'POST'])] public function namedRouteOperationIdAction() { } /** - * @Route("/custom-operation-id", methods={"GET", "POST"}) - * * @Operation(operationId="custom-operation-id") * @OA\Response(response=200, description="success") */ + #[Route(path: '/custom-operation-id', methods: ['GET', 'POST'])] public function customOperationIdAction() { } diff --git a/Tests/Functional/Controller/ArrayItemsErrorController.php b/Tests/Functional/Controller/ArrayItemsErrorController.php index 00c5751d3..a9895e752 100644 --- a/Tests/Functional/Controller/ArrayItemsErrorController.php +++ b/Tests/Functional/Controller/ArrayItemsErrorController.php @@ -16,19 +16,17 @@ use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route(host="api.example.com") - */ +#[Route(host: 'api.example.com')] class ArrayItemsErrorController { /** - * @Route("/api/error", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=Foo::class) * ) */ + #[Route(path: '/api/error', methods: ['GET'])] public function errorAction() { } diff --git a/Tests/Functional/Controller/BazingaController.php b/Tests/Functional/Controller/BazingaController.php index f8bb7d670..fcbe8fd07 100644 --- a/Tests/Functional/Controller/BazingaController.php +++ b/Tests/Functional/Controller/BazingaController.php @@ -16,31 +16,29 @@ use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route(host="api.example.com") - */ +#[Route(host: 'api.example.com')] class BazingaController { /** - * @Route("/api/bazinga", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=BazingaUser::class) * ) */ + #[Route(path: '/api/bazinga', methods: ['GET'])] public function userAction() { } /** - * @Route("/api/bazinga_foo", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=BazingaUser::class, groups={"foo"}) * ) */ + #[Route(path: '/api/bazinga_foo', methods: ['GET'])] public function userGroupAction() { } diff --git a/Tests/Functional/Controller/BazingaTypedController.php b/Tests/Functional/Controller/BazingaTypedController.php index ad72687f3..271ebe104 100644 --- a/Tests/Functional/Controller/BazingaTypedController.php +++ b/Tests/Functional/Controller/BazingaTypedController.php @@ -16,19 +16,17 @@ use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route(host="api.example.com") - */ +#[Route(host: 'api.example.com')] class BazingaTypedController { /** - * @Route("/api/bazinga_typed", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=BazingaUserTyped::class) * ) */ + #[Route(path: '/api/bazinga_typed', methods: ['GET'])] public function userTypedAction() { } diff --git a/Tests/Functional/Controller/ClassApiController.php b/Tests/Functional/Controller/ClassApiController.php index 21bbd0e45..8687da2fb 100644 --- a/Tests/Functional/Controller/ClassApiController.php +++ b/Tests/Functional/Controller/ClassApiController.php @@ -16,15 +16,15 @@ use Symfony\Component\Routing\Annotation\Route; /** - * @Route("/api", host="api.example.com") * @Security(name="basic") */ +#[Route(path: '/api', host: 'api.example.com')] class ClassApiController { /** - * @Route("/security/class") * @OA\Response(response="201", description="") */ + #[Route(path: '/security/class')] public function securityAction() { } diff --git a/Tests/Functional/Controller/FOSRestController.php b/Tests/Functional/Controller/FOSRestController.php index 772af464d..cb3538a39 100644 --- a/Tests/Functional/Controller/FOSRestController.php +++ b/Tests/Functional/Controller/FOSRestController.php @@ -18,13 +18,10 @@ use Symfony\Component\Validator\Constraints\IsTrue; use Symfony\Component\Validator\Constraints\Regex; -/** - * @Route("/api", host="api.example.com") - */ +#[Route(path: '/api', host: 'api.example.com')] class FOSRestController { /** - * @Route("/fosrest.{_format}", methods={"POST"}) * @QueryParam(name="foo", requirements=@Regex("/^\d+$/")) * @QueryParam(name="mapped", map=true) * @RequestParam(name="Barraa", key="bar", requirements="\d+") @@ -34,6 +31,7 @@ class FOSRestController * @RequestParam(name="datetimeNoFormat", requirements=@DateTime()) * @RequestParam(name="date", requirements=@DateTime("Y-m-d")) */ + #[Route(path: '/fosrest.{_format}', methods: ['POST'])] public function fosrestAction() { } diff --git a/Tests/Functional/Controller/InvokableController.php b/Tests/Functional/Controller/InvokableController.php index b640b70be..8f78f946e 100644 --- a/Tests/Functional/Controller/InvokableController.php +++ b/Tests/Functional/Controller/InvokableController.php @@ -17,12 +17,12 @@ /** * 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!" * ) */ +#[Route(path: '/api/invoke', host: 'api.example.com', name: 'invokable', methods: ['GET'])] class InvokableController { public function __invoke() diff --git a/Tests/Functional/Controller/JMSController.php b/Tests/Functional/Controller/JMSController.php index 392eace10..1c68521da 100644 --- a/Tests/Functional/Controller/JMSController.php +++ b/Tests/Functional/Controller/JMSController.php @@ -24,115 +24,113 @@ use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route(host="api.example.com") - */ +#[Route(host: 'api.example.com')] class JMSController { /** - * @Route("/api/jms", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSUser::class) * ) */ + #[Route(path: '/api/jms', methods: ['GET'])] public function userAction() { } /** - * @Route("/api/yaml", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=VirtualProperty::class) * ) */ + #[Route(path: '/api/yaml', methods: ['GET'])] public function yamlAction() { } /** - * @Route("/api/jms_complex", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSComplex::class, groups={"list", "details", "User" : {"list"}}) * ) */ + #[Route(path: '/api/jms_complex', methods: ['GET'])] public function complexAction() { } /** - * @Route("/api/jms_complex_dual", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSDualComplex::class, groups={"Default", "complex" : {"User" : {"details"}}}) * ) */ + #[Route(path: '/api/jms_complex_dual', methods: ['GET'])] public function complexDualAction() { } /** - * @Route("/api/jms_naming_strategy", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSNamingStrategyConstraints::class, groups={"Default"}) * ) */ + #[Route(path: '/api/jms_naming_strategy', methods: ['GET'])] public function namingStrategyConstraintsAction() { } /** - * @Route("/api/jms_chat", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSChat::class, groups={"Default", "members" : {"mini"}}) * ) */ + #[Route(path: '/api/jms_chat', methods: ['GET'])] public function chatAction() { } /** - * @Route("/api/jms_picture", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSPicture::class, groups={"mini"}) * ) */ + #[Route(path: '/api/jms_picture', methods: ['GET'])] public function pictureAction() { } /** - * @Route("/api/jms_mini_user", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSChatUser::class, groups={"mini"}) * ) */ + #[Route(path: '/api/jms_mini_user', methods: ['GET'])] public function minUserAction() { } /** - * @Route("/api/jms_mini_user_nested", methods={"GET"}) * @OA\Response( * response=200, * description="Success", * @Model(type=JMSChatRoomUser::class, groups={"mini", "friend": {"living":{"Default"}}}) * ) */ + #[Route(path: '/api/jms_mini_user_nested', methods: ['GET'])] public function minUserNestedAction() { } diff --git a/Tests/Functional/Controller/SerializedNameController.php b/Tests/Functional/Controller/SerializedNameController.php index 736a2669f..18b67f775 100644 --- a/Tests/Functional/Controller/SerializedNameController.php +++ b/Tests/Functional/Controller/SerializedNameController.php @@ -18,9 +18,8 @@ /** * This controller is only loaded when SerializedName exists (sf >= 4.2). - * - * @Route("/api", host="api.example.com") */ +#[Route(path: '/api', host: 'api.example.com')] class SerializedNameController { /** @@ -29,8 +28,8 @@ class SerializedNameController * description="success", * @Model(type=SerializedNameEnt::class) * ) - * @Route("/serializename", methods={"GET"}) */ + #[Route(path: '/serializename', methods: ['GET'])] public function serializedNameAction() { } diff --git a/Tests/Functional/Controller/TestController.php b/Tests/Functional/Controller/TestController.php index 7e887e613..e96045902 100644 --- a/Tests/Functional/Controller/TestController.php +++ b/Tests/Functional/Controller/TestController.php @@ -14,9 +14,7 @@ use OpenApi\Annotations as OA; use Symfony\Component\Routing\Annotation\Route; -/** - * @Route("/test", host="api-test.example.com") - */ +#[Route(path: '/test', host: 'api-test.example.com')] class TestController { /** @@ -24,8 +22,8 @@ class TestController * response="200", * description="Test" * ) - * @Route("/test/", methods={"GET"}) */ + #[Route(path: '/test/', methods: ['GET'])] public function testAction() { } diff --git a/Tests/Functional/Controller/UndocumentedController.php b/Tests/Functional/Controller/UndocumentedController.php index 98fbe6562..7c5c35b00 100644 --- a/Tests/Functional/Controller/UndocumentedController.php +++ b/Tests/Functional/Controller/UndocumentedController.php @@ -13,16 +13,13 @@ use Symfony\Component\Routing\Annotation\Route; -/** - * @Route(host="api.example.com") - */ +#[Route(host: 'api.example.com')] class UndocumentedController { /** * This path is excluded by the config (only /api allowed). - * - * @Route("/undocumented", methods={"GET"}) */ + #[Route(path: '/undocumented', methods: ['GET'])] public function undocumentedAction() { } diff --git a/Tests/Functional/Entity/Article.php b/Tests/Functional/Entity/Article.php index 922e54620..fe44c45e5 100644 --- a/Tests/Functional/Entity/Article.php +++ b/Tests/Functional/Entity/Article.php @@ -18,9 +18,7 @@ */ class Article { - /** - * @Groups({"light"}) - */ + #[Groups(groups: ['light'])] public function setAuthor(User $author) { } diff --git a/Tests/Functional/Entity/Dummy.php b/Tests/Functional/Entity/Dummy.php index 1a07b9ade..1987bba15 100644 --- a/Tests/Functional/Entity/Dummy.php +++ b/Tests/Functional/Entity/Dummy.php @@ -37,9 +37,9 @@ class Dummy /** * @var string * - * @Assert\NotBlank * @ApiProperty(iri="http://schema.org/name") */ + #[Assert\NotBlank] private $name; public function getId(): int diff --git a/Tests/Functional/Entity/JMSNamingStrategyConstraints.php b/Tests/Functional/Entity/JMSNamingStrategyConstraints.php index 1103e20eb..376579920 100644 --- a/Tests/Functional/Entity/JMSNamingStrategyConstraints.php +++ b/Tests/Functional/Entity/JMSNamingStrategyConstraints.php @@ -21,11 +21,10 @@ class JMSNamingStrategyConstraints * * @Serializer\Type("string") * @Serializer\SerializedName("beautifulName") - * - * @Assert\NotBlank() - * @Assert\Regex(pattern="\w+") - * @Assert\Length(min="3", max="10") */ + #[Assert\NotBlank] + #[Assert\Regex(pattern: '\w+')] + #[Assert\Length(min: 3, max: 10)] private $some_weird_named_property = 'default'; public function getSomeWeirdNamedProperty(): string diff --git a/Tests/Functional/Entity/SymfonyConstraints.php b/Tests/Functional/Entity/SymfonyConstraints.php index 05e65c952..f47043d45 100644 --- a/Tests/Functional/Entity/SymfonyConstraints.php +++ b/Tests/Functional/Entity/SymfonyConstraints.php @@ -18,96 +18,80 @@ class SymfonyConstraints { /** * @var int - * - * @Assert\NotBlank() */ + #[Assert\NotBlank] private $propertyNotBlank; /** * @var int - * - * @Assert\NotNull() */ + #[Assert\NotNull] private $propertyNotNull; /** * @var int - * - * @Assert\Length(min="0", max="50") */ + #[Assert\Length(min: 0, max: 50)] private $propertyAssertLength; /** * @var int - * - * @Assert\Regex(pattern="/[a-z]{2}/") */ + #[Assert\Regex(pattern: '/[a-z]{2}/')] private $propertyRegex; /** * @var int - * - * @Assert\Count(min="0", max="10") */ + #[Assert\Count(min: 0, max: 10)] private $propertyCount; /** * @var int - * - * @Assert\Choice(choices={"choice1", "choice2"}) */ + #[Assert\Choice(choices: ['choice1', 'choice2'])] private $propertyChoice; /** * @var int - * - * @Assert\Choice(callback={SymfonyConstraints::class,"fetchAllowedChoices"}) */ + #[Assert\Choice(callback: [SymfonyConstraints::class, 'fetchAllowedChoices'])] private $propertyChoiceWithCallback; /** * @var int - * - * @Assert\Choice(callback="fetchAllowedChoices") */ + #[Assert\Choice(callback: 'fetchAllowedChoices')] private $propertyChoiceWithCallbackWithoutClass; /** * @var string[] - * - * @Assert\Choice(multiple=true, choices={"choice1", "choice2"}) */ + #[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!" - * ) */ + #[Assert\Expression(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) */ + #[Assert\Range(min: 1, max: 5)] private $propertyRange; /** * @var int - * - * @Assert\LessThan(42) */ + #[Assert\LessThan(value: 42)] private $propertyLessThan; /** * @var int - * - * @Assert\LessThanOrEqual(23) */ + #[Assert\LessThanOrEqual(value: 23)] private $propertyLessThanOrEqual; /** @@ -122,9 +106,7 @@ public function setPropertyWithCompoundValidationRule(int $propertyWithCompoundV $this->propertyWithCompoundValidationRule = $propertyWithCompoundValidationRule; } - /** - * @Assert\Count(min="0", max="10") - */ + #[Assert\Count(min: 0, max: 10)] public function setPropertyNotBlank(int $propertyNotBlank): void { $this->propertyNotBlank = $propertyNotBlank; diff --git a/Tests/Functional/FOSRestTest.php b/Tests/Functional/FOSRestTest.php index 66917b829..cd1cfbe11 100644 --- a/Tests/Functional/FOSRestTest.php +++ b/Tests/Functional/FOSRestTest.php @@ -12,6 +12,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use OpenApi\Annotations as OA; +use OpenApi\Generator; class FOSRestTest extends WebTestCase { @@ -37,17 +38,17 @@ public function testFOSRestAction() $fooParameter = $this->getParameter($operation, 'foo', 'query'); $this->assertInstanceOf(OA\Schema::class, $fooParameter->schema); $this->assertEquals('\d+', $fooParameter->schema->pattern); - $this->assertEquals(OA\UNDEFINED, $fooParameter->schema->format); + $this->assertEquals(Generator::UNDEFINED, $fooParameter->schema->format); $mappedParameter = $this->getParameter($operation, 'mapped[]', 'query'); $this->assertTrue($mappedParameter->explode); $barProperty = $this->getProperty($bodySchema, 'bar'); $this->assertEquals('\d+', $barProperty->pattern); - $this->assertEquals(OA\UNDEFINED, $barProperty->format); + $this->assertEquals(Generator::UNDEFINED, $barProperty->format); $bazProperty = $this->getProperty($bodySchema, 'baz'); - $this->assertEquals(OA\UNDEFINED, $bazProperty->pattern); + $this->assertEquals(Generator::UNDEFINED, $bazProperty->pattern); $this->assertEquals('IsTrue', $bazProperty->format); $dateTimeProperty = $this->getProperty($bodySchema, 'datetime'); @@ -57,7 +58,7 @@ public function testFOSRestAction() $this->assertEquals('date-time', $dateTimeAltProperty->format); $dateTimeNoFormatProperty = $this->getProperty($bodySchema, 'datetimeNoFormat'); - $this->assertEquals(OA\UNDEFINED, $dateTimeNoFormatProperty->format); + $this->assertEquals(Generator::UNDEFINED, $dateTimeNoFormatProperty->format); $dateProperty = $this->getProperty($bodySchema, 'date'); $this->assertEquals('date', $dateProperty->format); diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 609446633..0704bc6a8 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -14,6 +14,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use Nelmio\ApiDocBundle\Tests\Helper; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Component\Serializer\Annotation\SerializedName; class FunctionalTest extends WebTestCase @@ -84,7 +85,7 @@ public function swaggerActionPathsProvider() public function testAnnotationWithManualPath() { $path = $this->getPath('/api/swagger2'); - $this->assertSame(OA\UNDEFINED, $path->post); + $this->assertSame(Generator::UNDEFINED, $path->post); $operation = $this->getOperation('/api/swagger', 'get'); $this->assertNotHasParameter('Accept-Version', 'header', $operation); @@ -123,10 +124,10 @@ public function testUserAction() { $operation = $this->getOperation('/api/test/{user}', 'get'); - $this->assertEquals(OA\UNDEFINED, $operation->security); - $this->assertEquals(OA\UNDEFINED, $operation->summary); - $this->assertEquals(OA\UNDEFINED, $operation->description); - $this->assertEquals(OA\UNDEFINED, $operation->deprecated); + $this->assertEquals(Generator::UNDEFINED, $operation->security); + $this->assertEquals(Generator::UNDEFINED, $operation->summary); + $this->assertEquals(Generator::UNDEFINED, $operation->description); + $this->assertEquals(Generator::UNDEFINED, $operation->deprecated); $this->assertHasResponse(200, $operation); $this->assertHasParameter('user', 'path', $operation); @@ -134,7 +135,7 @@ public function testUserAction() $this->assertTrue($parameter->required); $this->assertEquals('string', $parameter->schema->type); $this->assertEquals('/foo/', $parameter->schema->pattern); - $this->assertEquals(OA\UNDEFINED, $parameter->schema->format); + $this->assertEquals(Generator::UNDEFINED, $parameter->schema->format); } public function testDeprecatedAction() @@ -551,7 +552,7 @@ public function testModelsWithDiscriminatorMapAreLoadedWithOpenApiPolymorphism() $this->assertCount(2, $model->discriminator->mapping); $this->assertArrayHasKey('one', $model->discriminator->mapping); $this->assertArrayHasKey('two', $model->discriminator->mapping); - $this->assertNotSame(OA\UNDEFINED, $model->oneOf); + $this->assertNotSame(Generator::UNDEFINED, $model->oneOf); $this->assertCount(2, $model->oneOf); } diff --git a/Tests/Functional/JMSFunctionalTest.php b/Tests/Functional/JMSFunctionalTest.php index 221800df1..9bcc3c3b4 100644 --- a/Tests/Functional/JMSFunctionalTest.php +++ b/Tests/Functional/JMSFunctionalTest.php @@ -11,6 +11,8 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; +use Symfony\Component\HttpKernel\KernelInterface; + class JMSFunctionalTest extends WebTestCase { protected function setUp(): void @@ -333,7 +335,7 @@ public function testNamingStrategyWithConstraints() ], json_decode($this->getModel('JMSNamingStrategyConstraints')->toJson(), true)); } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return new TestKernel(TestKernel::USE_JMS); } diff --git a/Tests/Functional/TestKernel.php b/Tests/Functional/TestKernel.php index 5de564d2b..eb3622466 100644 --- a/Tests/Functional/TestKernel.php +++ b/Tests/Functional/TestKernel.php @@ -53,7 +53,7 @@ public function __construct(int $flags = 0) /** * {@inheritdoc} */ - public function registerBundles() + public function registerBundles(): iterable { $bundles = [ new FrameworkBundle(), @@ -106,7 +106,7 @@ protected function configureRoutes(RouteCollectionBuilder $routes) try { new \ReflectionMethod(Embedded::class, 'getType'); $routes->import(__DIR__.'/Controller/BazingaTypedController.php', '/', 'annotation'); - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { } } @@ -275,7 +275,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load /** * {@inheritdoc} */ - public function getCacheDir() + public function getCacheDir(): string { return parent::getCacheDir().'/'.$this->flags; } @@ -283,7 +283,7 @@ public function getCacheDir() /** * {@inheritdoc} */ - public function getLogDir() + public function getLogDir(): string { return parent::getLogDir().'/'.$this->flags; } diff --git a/Tests/Functional/WebTestCase.php b/Tests/Functional/WebTestCase.php index 2268de2d0..c792c0d2c 100644 --- a/Tests/Functional/WebTestCase.php +++ b/Tests/Functional/WebTestCase.php @@ -12,11 +12,13 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use OpenApi\Annotations as OA; +use OpenApi\Generator; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; +use Symfony\Component\HttpKernel\KernelInterface; class WebTestCase extends BaseWebTestCase { - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return new TestKernel(); } @@ -93,7 +95,7 @@ protected function getPath($path): OA\PathItem public function assertHasPath($path, OA\OpenApi $api) { - $paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path'); + $paths = array_column(Generator::UNDEFINED !== $api->paths ? $api->paths : [], 'path'); static::assertContains( $path, $paths, @@ -103,7 +105,7 @@ public function assertHasPath($path, OA\OpenApi $api) public function assertNotHasPath($path, OA\OpenApi $api) { - $paths = array_column(OA\UNDEFINED !== $api->paths ? $api->paths : [], 'path'); + $paths = array_column(Generator::UNDEFINED !== $api->paths ? $api->paths : [], 'path'); static::assertNotContains( $path, $paths, @@ -113,7 +115,7 @@ public function assertNotHasPath($path, OA\OpenApi $api) public function assertHasResponse($responseCode, OA\Operation $operation) { - $responses = array_column(OA\UNDEFINED !== $operation->responses ? $operation->responses : [], 'response'); + $responses = array_column(Generator::UNDEFINED !== $operation->responses ? $operation->responses : [], 'response'); static::assertContains( $responseCode, $responses, @@ -124,7 +126,7 @@ public function assertHasResponse($responseCode, OA\Operation $operation) public function assertHasParameter($name, $in, OA\AbstractAnnotation $annotation) { /* @var OA\Operation|OA\OpenApi $annotation */ - $parameters = array_filter(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], function (OA\Parameter $parameter) use ($name, $in) { + $parameters = array_filter(Generator::UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], function (OA\Parameter $parameter) use ($name, $in) { return $parameter->name === $name && $parameter->in === $in; }); @@ -137,7 +139,7 @@ public function assertHasParameter($name, $in, OA\AbstractAnnotation $annotation public function assertNotHasParameter($name, $in, OA\AbstractAnnotation $annotation) { /* @var OA\Operation|OA\OpenApi $annotation */ - $parameters = array_column(OA\UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in'); + $parameters = array_column(Generator::UNDEFINED !== $annotation->parameters ? $annotation->parameters : [], 'name', 'in'); static::assertNotContains( $name, $parameters[$in] ?? [], @@ -148,7 +150,7 @@ public function assertNotHasParameter($name, $in, OA\AbstractAnnotation $annotat public function assertHasProperty($property, OA\AbstractAnnotation $annotation) { /* @var OA\Schema|OA\Property|OA\Items $annotation */ - $properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property'); + $properties = array_column(Generator::UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property'); static::assertContains( $property, $properties, @@ -159,7 +161,7 @@ public function assertHasProperty($property, OA\AbstractAnnotation $annotation) public function assertNotHasProperty($property, OA\AbstractAnnotation $annotation) { /* @var OA\Schema|OA\Property|OA\Items $annotation */ - $properties = array_column(OA\UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property'); + $properties = array_column(Generator::UNDEFINED !== $annotation->properties ? $annotation->properties : [], 'property'); static::assertNotContains( $property, $properties, diff --git a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php index 9ee7fc8bc..40e5bd0e3 100644 --- a/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php +++ b/Tests/ModelDescriber/Annotations/SymfonyConstraintAnnotationReaderTest.php @@ -16,6 +16,7 @@ use Nelmio\ApiDocBundle\Tests\Helper; use Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations\Fixture as CustomAssert; use OpenApi\Annotations as OA; +use OpenApi\Generator; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Constraints as Assert; @@ -24,14 +25,10 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase public function testUpdatePropertyFix1283() { $entity = new class() { - /** - * @Assert\NotBlank() - * @Assert\Length(min = 1) - */ + #[Assert\NotBlank] + #[Assert\Length(min: 1)] private $property1; - /** - * @Assert\NotBlank() - */ + #[Assert\NotBlank] private $property2; }; @@ -76,14 +73,10 @@ public function testOptionalProperty($entity) public function provideOptionalProperty(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\NotBlank(allowNull = true) - * @Assert\Length(min = 1) - */ + #[Assert\NotBlank(allowNull: true)] + #[Assert\Length(min: 1)] private $property1; - /** - * @Assert\NotBlank() - */ + #[Assert\NotBlank] private $property2; }]; @@ -124,10 +117,8 @@ public function provideAssertChoiceResultsInNumericArray(): iterable ]); yield 'Annotations' => [new class() { - /** - * @Assert\Length(min = 1) - * @Assert\Choice(choices=TEST_ASSERT_CHOICE_STATUSES) - */ + #[Assert\Length(min: 1)] + #[Assert\Choice(choices: 'TEST_ASSERT_CHOICE_STATUSES')] private $property1; }]; @@ -161,9 +152,7 @@ public function testMultipleChoiceConstraintsApplyEnumToItems($entity) public function provideMultipleChoiceConstraintsApplyEnumToItems(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Choice(choices={"one", "two"}, multiple=true) - */ + #[Assert\Choice(choices: ['one', 'two'], multiple: true)] private $property1; }]; @@ -190,16 +179,14 @@ public function testLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet($entity) $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->maxLength); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maxLength); $this->assertSame(1, $schema->properties[0]->minLength); } public function provideLengthConstraintDoesNotSetMaxLengthIfMaxIsNotSet(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Length(min = 1) - */ + #[Assert\Length(min: 1)] private $property1; }]; @@ -226,16 +213,14 @@ public function testLengthConstraintDoesNotSetMinLengthIfMinIsNotSet($entity) $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->minLength); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minLength); $this->assertSame(100, $schema->properties[0]->maxLength); } public function provideLengthConstraintDoesNotSetMinLengthIfMinIsNotSet(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Length(max = 100) - */ + #[Assert\Length(max: 100)] private $property1; }]; @@ -272,11 +257,11 @@ public function testCompoundValidationRules() $this->assertSame(5, $schema->properties[0]->maximum); $this->assertTrue($schema->properties[0]->exclusiveMaximum); } else { - $this->assertSame(OA\UNDEFINED, $schema->required); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->minimum); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->exclusiveMinimum); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->maximum); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->exclusiveMaximum); + $this->assertSame(Generator::UNDEFINED, $schema->required); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minimum); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->exclusiveMinimum); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maximum); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->exclusiveMaximum); } } @@ -295,16 +280,14 @@ public function testCountConstraintDoesNotSetMinItemsIfMinIsNotSet($entity) $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->minItems); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minItems); $this->assertSame(10, $schema->properties[0]->maxItems); } public function provideCountConstraintDoesNotSetMinItemsIfMinIsNotSet(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Count(max = 10) - */ + #[Assert\Count(max: 10)] private $property1; }]; @@ -331,16 +314,14 @@ public function testCountConstraintDoesNotSetMaxItemsIfMaxIsNotSet($entity) $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->maxItems); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maxItems); $this->assertSame(10, $schema->properties[0]->minItems); } public function provideCountConstraintDoesNotSetMaxItemsIfMaxIsNotSet(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Count(min = 10) - */ + #[Assert\Count(min: 10)] private $property1; }]; @@ -367,16 +348,14 @@ public function testRangeConstraintDoesNotSetMaximumIfMaxIsNotSet($entity) $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->maximum); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->maximum); $this->assertSame(10, $schema->properties[0]->minimum); } public function provideRangeConstraintDoesNotSetMaximumIfMaxIsNotSet(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Range(min = 10) - */ + #[Assert\Range(min: 10)] private $property1; }]; @@ -403,16 +382,14 @@ public function testRangeConstraintDoesNotSetMinimumIfMinIsNotSet($entity) $symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]); - $this->assertSame(OA\UNDEFINED, $schema->properties[0]->minimum); + $this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minimum); $this->assertSame(10, $schema->properties[0]->maximum); } public function provideRangeConstraintDoesNotSetMinimumIfMinIsNotSet(): iterable { yield 'Annotations' => [new class() { - /** - * @Assert\Range(max = 10) - */ + #[Assert\Range(max: 10)] private $property1; }]; diff --git a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php index 51df5e407..cf65b8af1 100644 --- a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php +++ b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php @@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Model\ModelRegistry; use Nelmio\ApiDocBundle\ModelDescriber\ApplyOpenApiDiscriminatorTrait; use OpenApi\Annotations as OA; +use OpenApi\Generator; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -57,7 +58,7 @@ public function testApplyAddsOneOfFieldToSchema() 'two' => 'SecondType', ]); - $this->assertNotSame(OA\UNDEFINED, $this->schema->oneOf); + $this->assertNotSame(Generator::UNDEFINED, $this->schema->oneOf); $this->assertCount(2, $this->schema->oneOf); $this->assertSame( $this->modelRegistry->register($this->createModel('FirstType')), diff --git a/Tests/Render/RenderOpenApiTest.php b/Tests/Render/RenderOpenApiTest.php index d24e4f7cd..464c18fa7 100644 --- a/Tests/Render/RenderOpenApiTest.php +++ b/Tests/Render/RenderOpenApiTest.php @@ -65,11 +65,8 @@ private function renderOpenApi(...$openApiRenderer): void { $spec = $this->createMock(OpenApi::class); $generator = new class($spec) { - private $spec; - - public function __construct($spec) + public function __construct(private $spec) { - $this->spec = $spec; } public function generate() diff --git a/Tests/SwaggerPhp/UtilTest.php b/Tests/SwaggerPhp/UtilTest.php index 2b3bfc859..8f8bbca37 100644 --- a/Tests/SwaggerPhp/UtilTest.php +++ b/Tests/SwaggerPhp/UtilTest.php @@ -14,7 +14,7 @@ use Nelmio\ApiDocBundle\OpenApiPhp\Util; use OpenApi\Annotations as OA; use OpenApi\Context; -use const OpenApi\UNDEFINED; +use OpenApi\Generator; use PHPUnit\Framework\TestCase; /** @@ -104,10 +104,10 @@ public function testCreateChildWithEmptyProperties() $info = Util::createChild($this->rootAnnotation, OA\Info::class, $properties); $properties = array_filter(get_object_vars($info), function ($key) { - return 0 !== strpos($key, '_'); + return !str_starts_with($key, '_'); }, ARRAY_FILTER_USE_KEY); - $this->assertEquals([UNDEFINED], array_unique(array_values($properties))); + $this->assertEquals([Generator::UNDEFINED], array_unique(array_values($properties))); $this->assertIsNested($this->rootAnnotation, $info); $this->assertIsConnectedToRootContext($info); @@ -220,7 +220,7 @@ public function testSearchIndexedCollectionItem($setup, $asserts) foreach ($items as $assert) { $setupCollection = empty($assert['components']) ? ($setup[$collection] ?? []) : - (OA\UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []); + (Generator::UNDEFINED !== $setup['components']->{$collection} ? $setup['components']->{$collection} : []); // get the indexing correct within haystack preparation $properties = array_fill(0, \count($setupCollection), null); @@ -829,10 +829,10 @@ private function getSetupPropertiesWithoutClass(array $setup) private function getNonDefaultProperties($object) { $objectVars = \get_object_vars($object); - $classVars = \get_class_vars(\get_class($object)); + $classVars = \get_class_vars($object::class); $props = []; foreach ($objectVars as $key => $value) { - if ($value !== $classVars[$key] && 0 !== \strpos($key, '_')) { + if ($value !== $classVars[$key] && !str_starts_with($key, '_')) { $props[$key] = $value; } } diff --git a/Tests/Util/ControllerReflectorTest.php b/Tests/Util/ControllerReflectorTest.php new file mode 100644 index 000000000..83464cd30 --- /dev/null +++ b/Tests/Util/ControllerReflectorTest.php @@ -0,0 +1,29 @@ +assertEquals( + ReflectionMethod::class, + $controllerReflector->getReflectionMethod([BazingaController::class, 'userAction'])::class + ); + $this->assertEquals( + ReflectionMethod::class, + $controllerReflector->getReflectionMethod(BazingaController::class.'::userAction')::class + ); + $this->assertNull( + $controllerReflector->getReflectionMethod('UnknownController::userAction') + ); + $this->assertNull($controllerReflector->getReflectionMethod(null)); + } +} diff --git a/Util/ControllerReflector.php b/Util/ControllerReflector.php index 49fe9d7ad..4ee49e106 100644 --- a/Util/ControllerReflector.php +++ b/Util/ControllerReflector.php @@ -20,16 +20,11 @@ */ class ControllerReflector { - private $container; + private mixed $controllerNameParser; + private array $controllers = []; - private $controllerNameParser; - - private $controllers = []; - - public function __construct(ContainerInterface $container) + public function __construct(private ContainerInterface $container) { - $this->container = $container; - if (1 < \func_num_args() && func_get_arg(1) instanceof ControllerNameParser) { $this->controllerNameParser = func_get_arg(1); } @@ -37,29 +32,25 @@ public function __construct(ContainerInterface $container) /** * Returns the ReflectionMethod for the given controller string. - * - * @return \ReflectionMethod|null */ - public function getReflectionMethod($controller) + public function getReflectionMethod($controller): ?\ReflectionMethod { if (is_string($controller)) { $controller = $this->getClassAndMethod($controller); - if (null === $controller) { - return null; - } + } + + if (null === $controller) { + return null; } return $this->geReflectionMethodByClassNameAndMethodName(...$controller); } - /** - * @return \ReflectionMethod|null - */ - public function geReflectionMethodByClassNameAndMethodName(string $class, string $method) + public function geReflectionMethodByClassNameAndMethodName(string $class, string $method): ?\ReflectionMethod { try { return new \ReflectionMethod($class, $method); - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { // In case we can't reflect the controller, we just // ignore the route } @@ -73,14 +64,14 @@ private function getClassAndMethod(string $controller) return $this->controllers[$controller]; } - if ($this->controllerNameParser && false === strpos($controller, '::') && 2 === substr_count($controller, ':')) { + if ($this->controllerNameParser && !str_contains($controller, '::') && 2 === substr_count($controller, ':')) { $deprecatedNotation = $controller; try { $controller = $this->controllerNameParser->parse($controller); @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $deprecatedNotation, $controller), E_USER_DEPRECATED); - } catch (\InvalidArgumentException $e) { + } catch (\InvalidArgumentException) { // unable to optimize unknown notation } } @@ -91,7 +82,7 @@ private function getClassAndMethod(string $controller) // Since symfony 4.1 routes are defined like service_id::method_name if (Kernel::VERSION_ID >= 40100 && !class_exists($class)) { if ($this->container->has($class)) { - $class = get_class($this->container->get($class)); + $class = $this->container->get($class)::class; if (class_exists(ClassUtils::class)) { $class = ClassUtils::getRealClass($class); } @@ -108,7 +99,7 @@ private function getClassAndMethod(string $controller) } if ($this->container->has($controller)) { - $class = get_class($this->container->get($controller)); + $class = $this->container->get($controller)::class; if (class_exists(ClassUtils::class)) { $class = ClassUtils::getRealClass($class); } @@ -122,7 +113,7 @@ private function getClassAndMethod(string $controller) if (!isset($class) || !isset($method)) { $this->controllers[$controller] = null; - return; + return null; } return $this->controllers[$controller] = [$class, $method]; diff --git a/Util/SetsContextTrait.php b/Util/SetsContextTrait.php new file mode 100644 index 000000000..394de3cd4 --- /dev/null +++ b/Util/SetsContextTrait.php @@ -0,0 +1,24 @@ +=7.1.3", + "php": ">=8.0", "ext-json": "*", "doctrine/annotations": "^1.11", "psr/cache": "^1.0|^2.0|^3.0", "psr/container": "^1.0|^2.0", "psr/log": "^1.0|^2.0|^3.0", - "symfony/config": "^4.4|^5.0", - "symfony/console": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/framework-bundle": "^4.4|^5.0", - "symfony/http-foundation": "^4.4|^5.0", - "symfony/http-kernel": "^4.4|^5.0", - "symfony/options-resolver": "^4.4|^5.0", - "symfony/property-info": "^4.4|^5.0", - "symfony/routing": "^4.4|^5.0", - "zircote/swagger-php": "^3.0", + "symfony/config": "^5.3|^6.0", + "symfony/console": "^5.3|^6.0", + "symfony/dependency-injection": "^5.3|^6.0", + "symfony/framework-bundle": "^5.3|^6.0", + "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-kernel": "^5.3|^6.0", + "symfony/options-resolver": "^5.3|^6.0", + "symfony/property-info": "^5.3|^6.0", + "symfony/routing": "^5.3|^6.0", + "zircote/swagger-php": "^3.2|^4.0", "phpdocumentor/reflection-docblock": "^3.1|^4.4|^5.0" }, "require-dev": { - "sensio/framework-extra-bundle": "^4.4|^5.0|^6.0", - "symfony/asset": "^4.4|^5.0", - "symfony/dom-crawler": "^4.4|^5.0", - "symfony/browser-kit": "^4.4|^5.0", - "symfony/cache": "^4.4|^5.0", - "symfony/form": "^4.4|^5.0", - "symfony/phpunit-bridge": "^5.2", - "symfony/property-access": "^4.4|^5.0", - "symfony/serializer": "^4.4|^5.0", - "symfony/stopwatch": "^4.4|^5.0", - "symfony/templating": "^4.4|^5.0", - "symfony/twig-bundle": "^4.4|^5.0", - "symfony/validator": "^4.4|^5.0", + "sensio/framework-extra-bundle": "^5.3|^6.0", + "symfony/asset": "^5.3|^6.0", + "symfony/dom-crawler": "^5.3|^6.0", + "symfony/browser-kit": "^5.3|^6.0", + "symfony/cache": "^5.3|^6.0", + "symfony/form": "^5.3|^6.0", + "symfony/phpunit-bridge": "^5.3|^6.0", + "symfony/property-access": "^5.3|^6.0", + "symfony/serializer": "^5.3|^6.0", + "symfony/stopwatch": "^5.3|^6.0", + "symfony/templating": "^5.3|^6.0", + "symfony/twig-bundle": "^5.3|^6.0", + "symfony/validator": "^5.3|^6.0", "api-platform/core": "^2.4", - "friendsofsymfony/rest-bundle": "^2.8|^3.0", + "friendsofsymfony/rest-bundle": "^2.8|^3.0|dev-3.*", "willdurand/hateoas-bundle": "^1.0|^2.0", "jms/serializer-bundle": "^2.3|^3.0", "jms/serializer": "^1.14|^3.0",