Merge remote-tracking branch 'origin/master' into constraint_groups

This commit is contained in:
Guilhem Niot 2022-04-13 19:43:23 +02:00
commit fca94057d2
42 changed files with 982 additions and 389 deletions

View File

@ -34,6 +34,9 @@ jobs:
symfony-require: "5.4.*"
- php-version: 8.1
symfony-require: "5.4.*"
- php-version: 8.1
symfony-require: "6.0.*"
api-platform: "early"
steps:
- name: "Checkout"
@ -59,6 +62,14 @@ jobs:
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: "Use an early version of Api-platform with symfony 6 support"
if: ${{ matrix.api-platform == 'early' }}
env:
SYMFONY_REQUIRE: "${{ matrix.symfony-require }}"
run: |
composer config repositories.api-platform git https://github.com/PierreRebeilleau/core.git
composer require api-platform/core:dev-test-compatibility --no-update --dev
- name: "Install dependencies with composer"
env:
SYMFONY_REQUIRE: "${{ matrix.symfony-require }}"

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
/.phpunit.result.cache
/Tests/Functional/cache
/Tests/Functional/logs
.idea

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Annotation;
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Areas
{
/** @var string[] */
@ -22,6 +23,10 @@ final class Areas
public function __construct(array $properties)
{
if (!array_key_exists('value', $properties) || !is_array($properties['value'])) {
$properties['value'] = array_values($properties);
}
if ([] === $properties['value']) {
throw new \InvalidArgumentException('An array of areas was expected');
}

View File

@ -13,10 +13,12 @@ namespace Nelmio\ApiDocBundle\Annotation;
use OpenApi\Annotations\AbstractAnnotation;
use OpenApi\Annotations\Parameter;
use OpenApi\Generator;
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Model extends AbstractAnnotation
{
/** {@inheritdoc} */
@ -46,4 +48,22 @@ final class Model extends AbstractAnnotation
* @var mixed[]
*/
public $options;
/**
* @param mixed[] $properties
* @param string[] $groups
* @param mixed[] $options
*/
public function __construct(
array $properties = [],
string $type = Generator::UNDEFINED,
array $groups = null,
array $options = null
) {
parent::__construct($properties + [
'type' => $type,
'groups' => $groups,
'options' => $options,
]);
}
}

View File

@ -16,6 +16,7 @@ use OpenApi\Annotations\Operation as BaseOperation;
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_METHOD)]
class Operation extends BaseOperation
{
}

View File

@ -16,6 +16,7 @@ use OpenApi\Annotations\AbstractAnnotation;
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Security extends AbstractAnnotation
{
/** {@inheritdoc} */
@ -35,4 +36,15 @@ class Security extends AbstractAnnotation
* @var string[]
*/
public $scopes = [];
public function __construct(
array $properties = [],
string $name = null,
array $scopes = []
) {
parent::__construct($properties + [
'name' => $name,
'scopes' => $scopes,
]);
}
}

View File

@ -15,7 +15,6 @@ use Nelmio\ApiDocBundle\Describer\DescriberInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\ModelDescriber\ModelDescriberInterface;
use Nelmio\ApiDocBundle\OpenApiPhp\DefaultOperationId;
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use OpenApi\Analysis;
@ -84,6 +83,14 @@ final class ApiDocGenerator
}
}
$generator = new Generator();
// Remove OperationId processor as we use a lot of generated annotations which do not have enough information in their context
// to generate these ids properly.
// @see https://github.com/zircote/swagger-php/issues/1153
$generator->setProcessors(array_filter($generator->getProcessors(), function ($processor) {
return !$processor instanceof \OpenApi\Processors\OperationId;
}));
$this->openApi = new OpenApi([]);
$modelRegistry = new ModelRegistry($this->modelDescribers, $this->openApi, $this->alternativeNames);
if (null !== $this->logger) {
@ -97,7 +104,12 @@ final class ApiDocGenerator
$describer->describe($this->openApi);
}
$context = Util::createContext();
$context = Util::createContext(
// BC for for zircote/swagger-php < 4.2
method_exists($generator, 'getVersion')
? ['version' => $generator->getVersion()]
: []
);
$analysis = new Analysis([], $context);
$analysis->addAnnotation($this->openApi, $context);
@ -108,10 +120,7 @@ final class ApiDocGenerator
// Calculate the associated schemas
$modelRegistry->registerSchemas();
$defaultOperationIdProcessor = new DefaultOperationId();
$defaultOperationIdProcessor($analysis);
$analysis->process((new Generator())->getProcessors());
$analysis->process($generator->getProcessors());
$analysis->validate();
if (isset($item)) {

View File

@ -30,6 +30,9 @@ final class ApiPlatformDescriber extends ExternalDocDescriber
[DocumentationNormalizer::SPEC_VERSION => 3]
);
// TODO: remove this
// Temporary fix: zircote/swagger-php does no longer support 3.0.x with x > 0
unset($documentation['openapi']);
unset($documentation['basePath']);
return $documentation;

View File

@ -17,6 +17,7 @@ use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
use Nelmio\ApiDocBundle\Util\ControllerReflector;
use Nelmio\ApiDocBundle\Util\SetsContextTrait;
use OpenApi\Analysers\AttributeAnnotationFactory;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Psr\Log\LoggerInterface;
@ -67,12 +68,14 @@ final class OpenApiPhpDescriber
$classAnnotations = array_filter($this->annotationReader->getClassAnnotations($declaringClass), function ($v) {
return $v instanceof OA\AbstractAnnotation;
});
$classAnnotations = array_merge($classAnnotations, $this->getAttributesAsAnnotation($declaringClass, $context));
$classAnnotations[$declaringClass->getName()] = $classAnnotations;
}
$annotations = array_filter($this->annotationReader->getMethodAnnotations($method), function ($v) {
return $v instanceof OA\AbstractAnnotation;
});
$annotations = array_merge($annotations, $this->getAttributesAsAnnotation($method, $context));
if (0 === count($annotations) && 0 === count($classAnnotations[$declaringClass->getName()])) {
continue;
@ -107,6 +110,13 @@ final class OpenApiPhpDescriber
if ($annotation instanceof Security) {
$annotation->validate();
if (null === $annotation->name) {
$mergeProperties->security = [];
continue;
}
$mergeProperties->security[] = [$annotation->name => $annotation->scopes];
continue;
@ -190,4 +200,24 @@ final class OpenApiPhpDescriber
return $path;
}
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
*
* @return OA\AbstractAnnotation[]
*/
private function getAttributesAsAnnotation($reflection, \OpenApi\Context $context): array
{
// BC zircote/swagger-php < 4.0
if (!class_exists(AttributeAnnotationFactory::class)) {
return [];
}
$attributesFactory = new AttributeAnnotationFactory();
$attributes = $attributesFactory->build($reflection, $context);
// The attributes factory removes the context after executing so we need to set it back...
$this->setContext($context);
return $attributes;
}
}

View File

@ -39,8 +39,8 @@ class OpenApiAnnotationsReader
public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schema): void
{
/** @var OA\Schema $oaSchema */
if (!$oaSchema = $this->annotationsReader->getClassAnnotation($reflectionClass, OA\Schema::class)) {
/** @var OA\Schema|null $oaSchema */
if (!$oaSchema = $this->getAnnotation($reflectionClass, OA\Schema::class)) {
return;
}
@ -56,10 +56,8 @@ class OpenApiAnnotationsReader
public function getPropertyName($reflection, string $default): string
{
/** @var OA\Property $oaProperty */
if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) {
return $default;
} elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) {
/** @var OA\Property|null $oaProperty */
if (!$oaProperty = $this->getAnnotation($reflection, OA\Property::class)) {
return $default;
}
@ -78,10 +76,8 @@ class OpenApiAnnotationsReader
'filename' => $declaringClass->getFileName(),
]));
/** @var OA\Property $oaProperty */
if ($reflection instanceof \ReflectionProperty && !$oaProperty = $this->annotationsReader->getPropertyAnnotation($reflection, OA\Property::class)) {
return;
} elseif ($reflection instanceof \ReflectionMethod && !$oaProperty = $this->annotationsReader->getMethodAnnotation($reflection, OA\Property::class)) {
/** @var OA\Property|null $oaProperty */
if (!$oaProperty = $this->getAnnotation($reflection, OA\Property::class)) {
return;
}
$this->setContext(null);
@ -95,4 +91,28 @@ class OpenApiAnnotationsReader
$property->mergeProperties($oaProperty);
}
/**
* @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflection
*
* @return mixed
*/
private function getAnnotation($reflection, string $className)
{
if (\PHP_VERSION_ID >= 80100) {
if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
return $attribute->newInstance();
}
}
if ($reflection instanceof \ReflectionClass) {
return $this->annotationsReader->getClassAnnotation($reflection, $className);
} elseif ($reflection instanceof \ReflectionProperty) {
return $this->annotationsReader->getPropertyAnnotation($reflection, $className);
} elseif ($reflection instanceof \ReflectionMethod) {
return $this->annotationsReader->getMethodAnnotation($reflection, $className);
}
return null;
}
}

View File

@ -81,7 +81,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
);
$annotationsReader->updateDefinition($reflClass, $schema);
$discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class);
$discriminatorMap = $this->getAnnotation($reflClass, DiscriminatorMap::class);
if ($discriminatorMap && Generator::UNDEFINED === $schema->discriminator) {
$this->applyOpenApiDiscriminator(
$model,
@ -192,6 +192,23 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
throw new \Exception(sprintf('Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.', $types[0]->getBuiltinType(), $model->getType()->getClassName(), $propertyName));
}
/**
* @return mixed
*/
private function getAnnotation(\ReflectionClass $reflection, string $className)
{
if (false === class_exists($className)) {
return null;
}
if (\PHP_VERSION_ID >= 80000) {
if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
return $attribute->newInstance();
}
}
return $this->doctrineReader->getClassAnnotation($reflection, $className);
}
public function supports(Model $model): bool
{
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType() && class_exists($model->getType()->getClassName());

View File

@ -1,36 +0,0 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\OpenApiPhp;
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.
* See https://github.com/zircote/swagger-php/pull/483#issuecomment-360739260 for the solution used here.
*
* @internal
*/
final class DefaultOperationId
{
public function __invoke(Analysis $analysis)
{
$allOperations = $analysis->getAnnotationsOfType(OA\Operation::class);
foreach ($allOperations as $operation) {
if (Generator::UNDEFINED === $operation->operationId) {
$operation->operationId = null;
}
}
}
}

View File

@ -316,8 +316,6 @@ final class Util
*/
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);
}

View File

@ -341,6 +341,7 @@ If you need more complex features, take a look at:
customization
commands
faq
security
.. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html
.. _`willdurand/Hateoas`: https://github.com/willdurand/Hateoas

View File

@ -0,0 +1,43 @@
Security
========
A default security policy can be added in ``nelmio_api_doc.documentation.security``
.. code-block:: yaml
nelmio_api_doc:
documentation:
components:
securitySchemes:
Bearer:
type: http
scheme: bearer
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
security:
Bearer: []
This will add the Bearer security policy to all registered paths.
Overriding Specific Paths
-------------------------
The security policy can be overriden for a path using the ``@Security`` annotation.
.. code-block:: php
/**
* @Security(name="ApiKeyAuth")
*/
Notice at the bottom of the docblock is a ``@Security`` annotation with a name of `ApiKeyAuth`. This will override the global security policy to only accept the ``ApiKeyAuth`` policy for this path.
You can also completely remove security from a path by providing ``@Security`` with a name of ``null``.
.. code-block:: php
/**
* @Security(name=null)
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -44,6 +44,8 @@ final class FosRestDescriber implements RouteDescriberInterface
$annotations = array_filter($annotations, static function ($value) {
return $value instanceof RequestParam || $value instanceof QueryParam;
});
$annotations = array_merge($annotations, $this->getAttributesAsAnnotation($reflectionMethod, RequestParam::class));
$annotations = array_merge($annotations, $this->getAttributesAsAnnotation($reflectionMethod, QueryParam::class));
foreach ($this->getOperations($api, $route) as $operation) {
foreach ($annotations as $annotation) {
@ -185,4 +187,21 @@ final class FosRestDescriber implements RouteDescriberInterface
$schema->format = $format;
}
}
/**
* @return OA\AbstractAnnotation[]
*/
private function getAttributesAsAnnotation(\ReflectionMethod $reflection, string $className): array
{
$annotations = [];
if (\PHP_VERSION_ID < 80100) {
return $annotations;
}
foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$annotations[] = $attribute->newInstance();
}
return $annotations;
}
}

View File

@ -129,6 +129,14 @@ final class FilteredRouteCollectionBuilder
return false;
}
/** @var Areas|null $areas */
$areas = $this->getAttributesAsAnnotation($reflectionMethod, Areas::class)[0] ?? null;
if (null === $areas) {
/** @var Areas|null $areas */
$areas = $this->getAttributesAsAnnotation($reflectionMethod->getDeclaringClass(), Areas::class)[0] ?? null;
if (null === $areas) {
/** @var Areas|null $areas */
$areas = $this->annotationReader->getMethodAnnotation(
$reflectionMethod,
@ -138,6 +146,8 @@ final class FilteredRouteCollectionBuilder
if (null === $areas) {
$areas = $this->annotationReader->getClassAnnotation($reflectionMethod->getDeclaringClass(), Areas::class);
}
}
}
return (null !== $areas) ? $areas->has($this->area) : false;
}
@ -168,4 +178,23 @@ final class FilteredRouteCollectionBuilder
return false;
}
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
*
* @return Areas[]
*/
private function getAttributesAsAnnotation($reflection, string $className): array
{
$annotations = [];
if (\PHP_VERSION_ID < 80100) {
return $annotations;
}
foreach ($reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$annotations[] = $attribute->newInstance();
}
return $annotations;
}
}

View File

@ -11,257 +11,20 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Areas;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType;
use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route;
if (\PHP_VERSION_ID >= 80100) {
/**
* @Route("/api", name="api_", host="api.example.com")
*/
class ApiController
class ApiController extends ApiController81
{
}
} else {
/**
* @OA\Get(
* @OA\Response(
* response="200",
* description="Success",
* @Model(type=Article::class, groups={"light"}))
* )
* )
* @OA\Parameter(ref="#/components/parameters/test")
* @Route("/article/{id}", methods={"GET"})
* @OA\Parameter(name="Accept-Version", in="header", @OA\Schema(type="string"))
* @OA\Parameter(name="Application-Name", in="header", @OA\Schema(type="string"))
* @Route("/api", name="api_", host="api.example.com")
*/
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")
* )
* @OA\Get(
* path="/api/swagger2",
* @OA\Parameter(name="Accept-Version", in="header", @OA\Schema(type="string"))
* )
* @OA\Post(
* path="/api/swagger2",
* @OA\Response(response="203", description="but 203 is not actually allowed (wrong method)")
* )
*/
public function swaggerAction()
{
}
/**
* @Route("/swagger/implicit", methods={"GET", "POST"})
* @OA\Response(
* response="201",
* description="Operation automatically detected",
* @Model(type=User::class)
* ),
* @OA\RequestBody(
* description="This is a request body",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=User::class))
* )
* )
* @OA\Tag(name="implicit")
*/
public function implicitSwaggerAction()
{
}
/**
* @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"})
* @OA\Response(
* response="201",
* description="Operation automatically detected",
* @Model(type=User::class)
* ),
* @OA\RequestBody(
* description="This is a request body",
* @Model(type=UserType::class, options={"bar": "baz"}))
* )
*/
public function submitUserTypeAction()
{
}
/**
* @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"})
* @OA\Response(response=200, description="sucessful")
*/
public function userAction()
{
}
/**
* This action is deprecated.
*
* Please do not use this action.
*
* @Route("/deprecated", methods={"GET"})
*
* @deprecated
*/
public function deprecatedAction()
{
}
/**
* This action is not documented. It is excluded by the config.
*
* @Route("/admin", methods={"GET"})
*/
public function adminAction()
{
}
/**
* @OA\Get(
* path="/filtered",
* @OA\Response(response="201", description="")
* )
*/
public function filteredAction()
{
}
/**
* @Route("/form", methods={"POST"})
* @OA\RequestBody(
* description="Request content",
* @Model(type=DummyType::class))
* )
* @OA\Response(response="201", description="")
*/
public function formAction()
{
}
/**
* @Route("/security")
* @OA\Response(response="201", description="")
* @Security(name="api_key")
* @Security(name="basic")
* @Security(name="oauth2", scopes={"scope_1"})
*/
public function securityAction()
{
}
/**
* @Route("/swagger/symfonyConstraints", methods={"GET"})
* @OA\Response(
* response="201",
* description="Used for symfony constraints test",
* @Model(type=SymfonyConstraints::class)
* )
*/
public function symfonyConstraintsAction()
{
}
/**
* @Route("/swagger/symfonyConstraintsWithValidationGroups", methods={"GET"})
* @OA\Response(
* response="201",
* description="Used for symfony constraints with validation groups test",
* @Model(type=SymfonyConstraintsWithValidationGroups::class, groups={"test"})
* )
*/
public function symfonyConstraintsWithGroupsAction()
{
}
/**
* @OA\Response(
* response="200",
* description="Success",
* ref="#/components/schemas/Test"
* ),
* @OA\Response(
* response="201",
* ref="#/components/responses/201"
* )
* @Route("/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))
*/
public function operationsWithOtherAnnotations()
{
}
/**
* @Route("/areas/new", methods={"GET", "POST"})
*
* @Areas({"area", "area2"})
*/
public function newAreaAction()
{
}
/**
* @Route("/compound", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="Worked well!", @Model(type=CompoundEntity::class))
*/
public function compoundEntityAction()
{
}
/**
* @Route("/discriminator-mapping", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="Worked well!", @Model(type=SymfonyDiscriminator::class))
*/
public function discriminatorMappingAction()
{
}
/**
* @Route("/named_route-operation-id", name="named_route_operation_id", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="success")
*/
public function namedRouteOperationIdAction()
{
}
/**
* @Route("/custom-operation-id", methods={"GET", "POST"})
*
* @Operation(operationId="custom-operation-id")
* @OA\Response(response=200, description="success")
*/
public function customOperationIdAction()
class ApiController extends ApiController80
{
}
}

View File

@ -0,0 +1,275 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Areas;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraintsWithValidationGroups;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType;
use OpenApi\Annotations as OA;
use Symfony\Component\Routing\Annotation\Route;
class ApiController80
{
/**
* @OA\Get(
* @OA\Response(
* response="200",
* description="Success",
* @Model(type=Article::class, groups={"light"}))
* )
* )
* @OA\Parameter(ref="#/components/parameters/test")
* @Route("/article/{id}", methods={"GET"})
* @OA\Parameter(name="Accept-Version", in="header", @OA\Schema(type="string"))
* @OA\Parameter(name="Application-Name", in="header", @OA\Schema(type="string"))
*/
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")
* )
* @OA\Get(
* path="/api/swagger2",
* @OA\Parameter(name="Accept-Version", in="header", @OA\Schema(type="string"))
* )
* @OA\Post(
* path="/api/swagger2",
* @OA\Response(response="203", description="but 203 is not actually allowed (wrong method)")
* )
*/
public function swaggerAction()
{
}
/**
* @Route("/swagger/implicit", methods={"GET", "POST"})
* @OA\Response(
* response="201",
* description="Operation automatically detected",
* @Model(type=User::class)
* ),
* @OA\RequestBody(
* description="This is a request body",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=User::class))
* )
* )
* @OA\Tag(name="implicit")
*/
public function implicitSwaggerAction()
{
}
/**
* @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"})
* @OA\Response(
* response="201",
* description="Operation automatically detected",
* @Model(type=User::class)
* ),
* @OA\RequestBody(
* description="This is a request body",
* @Model(type=UserType::class, options={"bar": "baz"}))
* )
*/
public function submitUserTypeAction()
{
}
/**
* @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"})
* @OA\Response(response=200, description="sucessful")
*/
public function userAction()
{
}
/**
* This action is deprecated.
*
* Please do not use this action.
*
* @Route("/deprecated", methods={"GET"})
*
* @deprecated
*/
public function deprecatedAction()
{
}
/**
* This action is not documented. It is excluded by the config.
*
* @Route("/admin", methods={"GET"})
*/
public function adminAction()
{
}
/**
* @OA\Get(
* path="/filtered",
* @OA\Response(response="201", description="")
* )
*/
public function filteredAction()
{
}
/**
* @Route("/form", methods={"POST"})
* @OA\RequestBody(
* description="Request content",
* @Model(type=DummyType::class))
* )
* @OA\Response(response="201", description="")
*/
public function formAction()
{
}
/**
* @Route("/security")
* @OA\Response(response="201", description="")
* @Security(name="api_key")
* @Security(name="basic")
* @Security(name="oauth2", scopes={"scope_1"})
*/
public function securityAction()
{
}
/**
* @Route("/securityOverride")
* @OA\Response(response="201", description="")
* @Security(name="api_key")
* @Security(name=null)
*/
public function securityActionOverride()
{
}
/**
* @Route("/swagger/symfonyConstraints", methods={"GET"})
* @OA\Response(
* response="201",
* description="Used for symfony constraints test",
* @Model(type=SymfonyConstraints::class)
* )
*/
public function symfonyConstraintsAction()
{
}
/**
* @OA\Response(
* response="200",
* description="Success",
* ref="#/components/schemas/Test"
* ),
* @OA\Response(
* response="201",
* ref="#/components/responses/201"
* )
* @Route("/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))
*/
public function operationsWithOtherAnnotations()
{
}
/**
* @Route("/areas/new", methods={"GET", "POST"})
*
* @Areas({"area", "area2"})
*/
public function newAreaAction()
{
}
/**
* @Route("/compound", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="Worked well!", @Model(type=CompoundEntity::class))
*/
public function compoundEntityAction()
{
}
/**
* @Route("/discriminator-mapping", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="Worked well!", @Model(type=SymfonyDiscriminator::class))
*/
public function discriminatorMappingAction()
{
}
/**
* @Route("/named_route-operation-id", name="named_route_operation_id", methods={"GET", "POST"})
*
* @OA\Response(response=200, description="success")
*/
public function namedRouteOperationIdAction()
{
}
/**
* @Route("/custom-operation-id", methods={"GET", "POST"})
*
* @OA\Get(operationId="get-custom-operation-id")
* @OA\Post(operationId="post-custom-operation-id")
* @OA\Response(response=200, description="success")
*/
public function customOperationIdAction()
{
}
/**
* @Route("/swagger/symfonyConstraintsWithValidationGroups", methods={"GET"})
* @OA\Response(
* response="201",
* description="Used for symfony constraints with validation groups test",
* @Model(type=SymfonyConstraintsWithValidationGroups::class, groups={"test"})
* )
*/
public function symfonyConstraintsWithGroupsAction()
{
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Areas;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
use OpenApi\Attributes as OA;
use Symfony\Component\Routing\Annotation\Route;
class ApiController81 extends ApiController80
{
#[OA\Get(responses: [
new OA\Response(
response: '200',
description: 'Success',
attachables: [
new Model(type: Article::class, groups: ['light']),
],
),
])]
#[OA\Parameter(ref: '#/components/parameters/test')]
#[Route('/article_attributes/{id}', methods: ['GET'])]
#[OA\Parameter(name: 'Accept-Version', in: 'header', attachables: [new OA\Schema(type: 'string')])]
public function fetchArticleActionWithAttributes()
{
}
#[Areas(['area', 'area2'])]
#[Route('/areas_attributes/new', methods: ['GET', 'POST'])]
public function newAreaActionAttributes()
{
}
#[Route('/security_attributes')]
#[OA\Response(response: '201', description: '')]
#[Security(name: 'api_key')]
#[Security(name: 'basic')]
#[Security(name: 'oauth2', scopes: ['scope_1'])]
public function securityActionAttributes()
{
}
#[Route('/security_override_attributes')]
#[OA\Response(response: '201', description: '')]
#[Security(name: 'api_key')]
#[Security(name: null)]
public function securityOverrideActionAttributes()
{
}
#[Route('/inline_path_parameters')]
#[OA\Response(response: '200', description: '')]
public function inlinePathParameters(
#[OA\PathParameter] string $product_id
) {
}
}

View File

@ -11,30 +11,20 @@
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\DateTime;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Regex;
if (\PHP_VERSION_ID >= 80100) {
/**
* @Route("/api", host="api.example.com")
*/
class FOSRestController
class FOSRestController extends FOSRestController81
{
}
} else {
/**
* @Route("/fosrest.{_format}", methods={"POST"})
* @QueryParam(name="foo", requirements=@Regex("/^\d+$/"))
* @QueryParam(name="mapped", map=true)
* @RequestParam(name="Barraa", key="bar", requirements="\d+")
* @RequestParam(name="baz", requirements=@IsTrue)
* @RequestParam(name="datetime", requirements=@DateTime("Y-m-d\TH:i:sP"))
* @RequestParam(name="datetimeAlt", requirements=@DateTime("c"))
* @RequestParam(name="datetimeNoFormat", requirements=@DateTime())
* @RequestParam(name="date", requirements=@DateTime("Y-m-d"))
* @Route("/api", host="api.example.com")
*/
public function fosrestAction()
class FOSRestController extends FOSRestController80
{
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\DateTime;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Regex;
class FOSRestController80
{
/**
* @Route("/fosrest.{_format}", methods={"POST"})
* @QueryParam(name="foo", requirements=@Regex("/^\d+$/"))
* @QueryParam(name="mapped", map=true)
* @RequestParam(name="Barraa", key="bar", requirements="\d+")
* @RequestParam(name="baz", requirements=@IsTrue)
* @RequestParam(name="datetime", requirements=@DateTime("Y-m-d\TH:i:sP"))
* @RequestParam(name="datetimeAlt", requirements=@DateTime("c"))
* @RequestParam(name="datetimeNoFormat", requirements=@DateTime())
* @RequestParam(name="date", requirements=@DateTime("Y-m-d"))
*/
public function fosrestAction()
{
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use FOS\RestBundle\Controller\Annotations\QueryParam;
use FOS\RestBundle\Controller\Annotations\RequestParam;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\DateTime;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Regex;
class FOSRestController81 extends FOSRestController80
{
#[Route('/fosrest_attributes.{_format}', methods: ['POST'])]
#[QueryParam(name: 'foo', requirements: new Regex('/^\d+$/'))]
#[QueryParam(name: 'mapped', map: true)]
#[RequestParam(name: 'Barraa', key: 'bar', requirements: '\d+')]
#[RequestParam(name: 'baz', requirements: new IsTrue())]
#[RequestParam(name: 'datetime', requirements: new DateTime('Y-m-d\TH:i:sP'))]
#[RequestParam(name: 'datetimeAlt', requirements: new DateTime('c'))]
#[RequestParam(name: 'datetimeNoFormat', requirements: new DateTime())]
#[RequestParam(name: 'date', requirements: new DateTime('Y-m-d'))]
public function fosrestAttributesAction()
{
}
}

View File

@ -23,9 +23,12 @@ class FOSRestTest extends WebTestCase
static::createClient([], ['HTTP_HOST' => 'api.example.com']);
}
public function testFOSRestAction()
/**
* @dataProvider provideRoute
*/
public function testFOSRestAction(string $route)
{
$operation = $this->getOperation('/api/fosrest', 'post');
$operation = $this->getOperation($route, 'post');
$this->assertHasParameter('foo', 'query', $operation);
$this->assertInstanceOf(OA\RequestBody::class, $operation->requestBody);
@ -66,4 +69,13 @@ class FOSRestTest extends WebTestCase
// The _format path attribute should be removed
$this->assertNotHasParameter('_format', 'path', $operation);
}
public function provideRoute(): iterable
{
yield 'Annotations' => ['/api/fosrest'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/fosrest_attributes'];
}
}
}

View File

@ -40,9 +40,12 @@ class FunctionalTest extends WebTestCase
$this->assertNotHasPath('/api/admin', $api);
}
public function testFetchArticleAction()
/**
* @dataProvider provideArticleRoute
*/
public function testFetchArticleAction(string $articleRoute)
{
$operation = $this->getOperation('/api/article/{id}', 'get');
$operation = $this->getOperation($articleRoute, 'get');
$this->assertHasResponse('200', $operation);
$response = $this->getOperationResponse($operation, '200');
@ -56,6 +59,15 @@ class FunctionalTest extends WebTestCase
$this->assertNotHasProperty('author', Util::getProperty($articleModel, 'author'));
}
public function provideArticleRoute(): iterable
{
yield 'Annotations' => ['/api/article/{id}'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/article_attributes/{id}'];
}
}
public function testFilteredAction()
{
$openApi = $this->getOpenApiDefinition();
@ -333,9 +345,12 @@ class FunctionalTest extends WebTestCase
], json_decode($this->getModel('DummyType')->toJson(), true));
}
public function testSecurityAction()
/**
* @dataProvider provideSecurityRoute
*/
public function testSecurityAction(string $route)
{
$operation = $this->getOperation('/api/security', 'get');
$operation = $this->getOperation($route, 'get');
$expected = [
['api_key' => []],
@ -345,6 +360,46 @@ class FunctionalTest extends WebTestCase
$this->assertEquals($expected, $operation->security);
}
public function provideSecurityRoute(): iterable
{
yield 'Annotations' => ['/api/security'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/security_attributes'];
}
}
/**
* @dataProvider provideSecurityOverrideRoute
*/
public function testSecurityOverrideAction(string $route)
{
$operation = $this->getOperation($route, 'get');
$this->assertEquals([], $operation->security);
}
public function provideSecurityOverrideRoute(): iterable
{
yield 'Annotations' => ['/api/securityOverride'];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => ['/api/security_override_attributes'];
}
}
public function testInlinePHP81Parameters()
{
if (\PHP_VERSION_ID < 80100) {
$this->markTestSkipped('Attributes require PHP 8.1');
}
$operation = $this->getOperation('/api/inline_path_parameters', 'get');
$this->assertCount(1, $operation->parameters);
$this->assertInstanceOf(OA\PathParameter::class, $operation->parameters[0]);
$this->assertSame($operation->parameters[0]->name, 'product_id');
$this->assertSame($operation->parameters[0]->schema->type, 'string');
}
public function testClassSecurityAction()
{
$operation = $this->getOperation('/api/security/class', 'get');
@ -522,10 +577,10 @@ class FunctionalTest extends WebTestCase
public function testCustomOperationId()
{
$operation = $this->getOperation('/api/custom-operation-id', 'get');
$this->assertEquals('custom-operation-id', $operation->operationId);
$this->assertEquals('get-custom-operation-id', $operation->operationId);
$operation = $this->getOperation('/api/custom-operation-id', 'post');
$this->assertEquals('custom-operation-id', $operation->operationId);
$this->assertEquals('post-custom-operation-id', $operation->operationId);
}
/**

View File

@ -0,0 +1,45 @@
# Resources
test:
resource: ../Controller/TestController.php
type: annotation
api:
resource: ../Controller/ApiController.php
type: annotation
class_api:
resource: ../Controller/ClassApiController.php
type: annotation
undocumented:
resource: ../Controller/UndocumentedController.php
type: annotation
invokable:
resource: ../Controller/InvokableController.php
type: annotation
fos_rest:
resource: ../Controller/FOSRestController.php
type: annotation
api_platform:
resource: .
prefix: /api
type: api_platform
# Controllers
doc_area:
path: /docs/{area}
controller: nelmio_api_doc.controller.swagger_ui
defaults:
area: default
doc_json:
path: /docs.json
controller: nelmio_api_doc.controller.swagger_json
doc_yaml:
path: /docs.yaml
controller: nelmio_api_doc.controller.swagger_yaml

View File

@ -31,7 +31,7 @@ use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Serializer\Annotation\SerializedName;
class TestKernel extends Kernel
@ -81,39 +81,42 @@ class TestKernel extends Kernel
/**
* {@inheritdoc}
*/
protected function configureRoutes(RouteCollectionBuilder $routes)
protected function configureRoutes($routes)
{
$routes->import(__DIR__.'/Controller/TestController.php', '/', 'annotation');
$routes->import(__DIR__.'/Controller/ApiController.php', '/', 'annotation');
$routes->import(__DIR__.'/Controller/ClassApiController.php', '/', 'annotation');
$routes->import(__DIR__.'/Controller/UndocumentedController.php', '/', 'annotation');
$routes->import(__DIR__.'/Controller/InvokableController.php', '/', 'annotation');
$routes->import('', '/api', 'api_platform');
$routes->add('/docs/{area}', 'nelmio_api_doc.controller.swagger_ui')->setDefault('area', 'default');
$routes->add('/docs.json', 'nelmio_api_doc.controller.swagger_json');
$routes->add('/docs.yaml', 'nelmio_api_doc.controller.swagger_yaml');
$routes->import(__DIR__.'/Controller/FOSRestController.php', '/', 'annotation');
$this->import($routes, __DIR__.'/Resources/routes.yaml', '/', 'yaml');
if (class_exists(SerializedName::class)) {
$routes->import(__DIR__.'/Controller/SerializedNameController.php', '/', 'annotation');
$this->import($routes, __DIR__.'/Controller/SerializedNameController.php', '/', 'annotation');
}
if ($this->flags & self::USE_JMS) {
$routes->import(__DIR__.'/Controller/JMSController.php', '/', 'annotation');
$this->import($routes, __DIR__.'/Controller/JMSController.php', '/', 'annotation');
}
if ($this->flags & self::USE_BAZINGA) {
$routes->import(__DIR__.'/Controller/BazingaController.php', '/', 'annotation');
$this->import($routes, __DIR__.'/Controller/BazingaController.php', '/', 'annotation');
try {
new \ReflectionMethod(Embedded::class, 'getType');
$routes->import(__DIR__.'/Controller/BazingaTypedController.php', '/', 'annotation');
$this->import($routes, __DIR__.'/Controller/BazingaTypedController.php', '/', 'annotation');
} catch (\ReflectionException $e) {
}
}
if ($this->flags & self::ERROR_ARRAY_ITEMS) {
$routes->import(__DIR__.'/Controller/ArrayItemsErrorController.php', '/', 'annotation');
$this->import($routes, __DIR__.'/Controller/ArrayItemsErrorController.php', '/', 'annotation');
}
}
/**
* BC for sf < 5.1.
*/
private function import($routes, $resource, $prefix, $type)
{
if ($routes instanceof RoutingConfigurator) {
$routes->withPath($prefix)->import($resource, $type);
} else {
$routes->import($resource, $prefix, $type);
}
}
@ -186,6 +189,20 @@ class TestKernel extends Kernel
'info' => [
'title' => 'My Default App',
],
'paths' => [
// Ensures we can define routes in Yaml without defining OperationIds
// See https://github.com/zircote/swagger-php/issues/1153
'/api/test-from-yaml' => ['get' => [
'responses' => [
200 => ['description' => 'success'],
],
]],
'/api/test-from-yaml2' => ['get' => [
'responses' => [
200 => ['description' => 'success'],
],
]],
],
'components' => [
'schemas' => [
'Test' => [

View File

@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
class WebTestCase extends BaseWebTestCase
@ -168,4 +169,16 @@ class WebTestCase extends BaseWebTestCase
sprintf('Failed asserting that property "%s" does not exist.', $property)
);
}
/**
* BC symfony < 5.3.
*/
protected static function getContainer(): ContainerInterface
{
if (method_exists(parent::class, 'getContainer')) {
return parent::getContainer();
}
return static::$container;
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\ModelDescriber\Annotations;
use Doctrine\Common\Annotations\AnnotationReader;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\OpenApiAnnotationsReader;
use OpenApi\Annotations as OA;
use OpenApi\Attributes as OAattr;
use OpenApi\Generator;
use PHPUnit\Framework\TestCase;
class AnnotationReaderTest extends TestCase
{
/**
* @param object $entity
* @dataProvider provideProperty
*/
public function testProperty($entity)
{
$schema = new OA\Schema([]);
$schema->merge([new OA\Property(['property' => 'property1'])]);
$schema->merge([new OA\Property(['property' => 'property2'])]);
$registry = new ModelRegistry([], new OA\OpenApi([]), []);
$symfonyConstraintAnnotationReader = new OpenApiAnnotationsReader(new AnnotationReader(), $registry, ['json']);
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property1'), $schema->properties[0]);
$symfonyConstraintAnnotationReader->updateProperty(new \ReflectionProperty($entity, 'property2'), $schema->properties[1]);
$this->assertEquals($schema->properties[0]->example, 1);
$this->assertEquals($schema->properties[0]->description, Generator::UNDEFINED);
$this->assertEquals($schema->properties[1]->example, 'some example');
$this->assertEquals($schema->properties[1]->description, 'some description');
}
public function provideProperty(): iterable
{
yield 'Annotations' => [new class() {
/**
* @OA\Property(example=1)
*/
private $property1;
/**
* @OA\Property(example="some example", description="some description")
*/
private $property2;
}];
if (\PHP_VERSION_ID >= 80100) {
yield 'Attributes' => [new class() {
#[OAattr\Property(example: 1)]
private $property1;
#[OAattr\Property(example: 'some example', description: 'some description')]
private $property2;
}];
}
}
}

View File

@ -469,7 +469,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
);
$this->assertSame(['property1'], $schema->required, 'should have read constraint in default group');
$this->assertSame(OA\UNDEFINED, $schema->properties[0]->minimum, 'should not have read constraint in other group');
$this->assertSame(Generator::UNDEFINED, $schema->properties[0]->minimum, 'should not have read constraint in other group');
}
/**
@ -492,7 +492,7 @@ class SymfonyConstraintAnnotationReaderTest extends TestCase
['other']
);
$this->assertSame(OA\UNDEFINED, $schema->required, 'should not have read constraint in default group');
$this->assertSame(Generator::UNDEFINED, $schema->required, 'should not have read constraint in default group');
$this->assertSame(1, $schema->properties[0]->minimum, 'should have read constraint in other group');
}

View File

@ -23,7 +23,7 @@ class GetNelmioAssetTest extends WebTestCase
{
static::bootKernel();
/** @var GetNelmioAsset $getNelmioAsset */
$getNelmioAsset = static::$container->get('nelmio_api_doc.render_docs.html.asset');
$getNelmioAsset = static::getContainer()->get('nelmio_api_doc.render_docs.html.asset');
/** @var TwigFunction */
$twigFunction = $getNelmioAsset->getFunctions()[0];
self::assertSame($expectedContent, $twigFunction->getCallable()->__invoke($mode, $asset));

View File

@ -157,9 +157,9 @@ class FilteredRouteCollectionBuilderTest extends TestCase
$this->assertCount(1, $filteredRoutes);
}
public function getMatchingRoutes(): array
public function getMatchingRoutes(): iterable
{
return [
yield from [
['r1', new Route('/api/bar/action1')],
['r2', new Route('/api/foo/action1'), ['path_patterns' => ['^/api', 'i/fo', 'n1$']]],
['r3', new Route('/api/foo/action2'), ['path_patterns' => ['^/api/foo/action2$']]],
@ -167,6 +167,10 @@ class FilteredRouteCollectionBuilderTest extends TestCase
['r9', new Route('/api/bar/action1', [], [], [], 'api.example.com'), ['path_patterns' => ['^/api/'], 'host_patterns' => ['^api\.ex']]],
['r10', new Route('/api/areas/new'), ['path_patterns' => ['^/api']]],
];
if (\PHP_VERSION_ID < 80000) {
yield ['r10', new Route('/api/areas_attributes/new'), ['path_patterns' => ['^/api']]];
}
}
/**
@ -201,9 +205,9 @@ class FilteredRouteCollectionBuilderTest extends TestCase
$this->assertCount(1, $filteredRoutes);
}
public function getMatchingRoutesWithAnnotation(): array
public function getMatchingRoutesWithAnnotation(): iterable
{
return [
yield from [
'with annotation only' => [
'r10',
new Route('/api/areas/new', ['_controller' => 'ApiController::newAreaAction']),
@ -215,6 +219,21 @@ class FilteredRouteCollectionBuilderTest extends TestCase
['path_patterns' => ['^/api'], 'with_annotation' => true],
],
];
if (\PHP_VERSION_ID < 80000) {
yield from [
'with attribute only' => [
'r10',
new Route('/api/areas_attributes/new', ['_controller' => 'ApiController::newAreaActionAttributes']),
['with_annotation' => true],
],
'with attribute and path patterns' => [
'r10',
new Route('/api/areas_attributes/new', ['_controller' => 'ApiController::newAreaActionAttributes']),
['path_patterns' => ['^/api'], 'with_annotation' => true],
],
];
}
}
/**

View File

@ -58,7 +58,7 @@ class UtilTest extends TestCase
{
$context = Util::createContext([], $this->rootContext);
$this->assertSame($this->rootContext, $context->getRootContext());
$this->assertContextIsConnectedToRootContext($context);
}
public function testCreateContextWithProperties()
@ -818,7 +818,20 @@ class UtilTest extends TestCase
public function assertIsConnectedToRootContext(OA\AbstractAnnotation $annotation)
{
$this->assertSame($this->rootContext, $annotation->_context->getRootContext());
$this->assertContextIsConnectedToRootContext($annotation->_context);
}
public function assertContextIsConnectedToRootContext(Context $context)
{
$getRootContext = \Closure::bind(function (Context $context) use (&$getRootContext) {
if (null !== $context->_parent) {
return $getRootContext($context->_parent);
}
return $context;
}, null, Context::class);
$this->assertSame($this->rootContext, $getRootContext($context));
}
private function getSetupPropertiesWithoutClass(array $setup)

View File

@ -13,14 +13,14 @@ The Upgrade to Swagger 3.0
The biggest part of the upgrade will most likely be the upgrade of the library `zircote/swagger-php` to `3.0` which introduces new annotations in order to support OpenAPI 3.0 but also changes
their namespace from ``Swagger`` to ``OpenApi``.
They created a dedicated page to help you upgrade : https://zircote.github.io/swagger-php/Migrating-to-v3.html.
They created a dedicated page to help you upgrade : https://github.com/zircote/swagger-php/blob/3.x/docs/Migrating-to-v3.md.
Here are some additional advices that are more likely to apply to NelmioApiDocBundle users:
- Upgrade all your ``use Swagger\Annotations as SWG`` statements to ``use OpenApi\Annotations as OA;`` (to simplify the upgrade you may also stick to the ``SWG`` aliasing).
In case you changed ``SWG`` to ``OA``, upgrade all your annotations from ``@SWG\...`` to ``@OA\...``.
- Update your config in case you used inlined swagger docummentation (the field ``nelmio_api_doc.documentation``). [A tool](https://openapi-converter.herokuapp.com/) is available to help you convert it.
- Update your config in case you used inlined swagger documentation (the field ``nelmio_api_doc.documentation``). [A tool](https://openapi-converter.herokuapp.com/) is available to help you convert it.
- In case you used ``@OA\Response(..., @OA\Schema(...))``, you should explicit your media type by using the annotation ``@OA\JsonContent`` or ``@OA\XmlContent`` instead of ``@OA\Schema``:
``@OA\Response(..., @OA\JsonContent(...))`` or ``@OA\Response(..., @OA\XmlContent(...))``.

View File

@ -15,7 +15,7 @@ trait SetsContextTrait
// zircote/swagger-php ^3.2
\OpenApi\Analyser::$context = $context;
} else {
/// zircote/swagger-php ^4.0
// zircote/swagger-php ^4.0
\OpenApi\Generator::$context = $context;
}
}

View File

@ -21,37 +21,37 @@
"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",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/console": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/framework-bundle": "^4.4|^5.0|^6.0",
"symfony/http-foundation": "^4.4|^5.0|^6.0",
"symfony/http-kernel": "^4.4|^5.0|^6.0",
"symfony/options-resolver": "^4.4|^5.0|^6.0",
"symfony/property-info": "^4.4|^5.0|^6.0",
"symfony/routing": "^4.4|^5.0|^6.0",
"zircote/swagger-php": "^3.2|^4.0",
"phpdocumentor/reflection-docblock": "^3.1|^4.4|^5.0"
"phpdocumentor/reflection-docblock": "^3.1|^4.0|^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",
"sensio/framework-extra-bundle": "^4.4|^5.2|^6.0",
"symfony/asset": "^4.4|^5.2|^6.0",
"symfony/dom-crawler": "^4.4|^5.2|^6.0",
"symfony/browser-kit": "^4.4|^5.2|^6.0",
"symfony/cache": "^4.4|^5.2|^6.0",
"symfony/form": "^4.4|^5.2|^6.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",
"symfony/property-access": "^4.4|^5.2|^6.0",
"symfony/serializer": "^4.4|^5.2|^6.0",
"symfony/stopwatch": "^4.4|^5.2|^6.0",
"symfony/templating": "^4.4|^5.2|^6.0",
"symfony/twig-bundle": "^4.4|^5.2|^6.0",
"symfony/validator": "^4.4|^5.2|^6.0",
"api-platform/core": "^2.4",
"friendsofsymfony/rest-bundle": "^2.8|^3.0",
"willdurand/hateoas-bundle": "^1.0|^2.0",
"jms/serializer-bundle": "^2.3|^3.0",
"jms/serializer-bundle": "^2.3|^3.0|^4.0",
"jms/serializer": "^1.14|^3.0",
"composer/package-versions-deprecated": "1.11.99.1"
},