mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-02 15:51:48 +03:00
Add support for php attributes (#1932)
* Add support for php attributes * Fix tests for php 8.1 * Simplify the annotations * Revert the changes to the tests * CS * Test FOSRest parsing of attributes * CS * typo * CS * Test fetchArticle php 8.1 attributes * Fix namespaces Co-authored-by: Guilhem Niot <guilhem@gniot.fr>
This commit is contained in:
parent
3d263a525d
commit
cc97b0ba45
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@
|
||||
/.phpunit.result.cache
|
||||
/Tests/Functional/cache
|
||||
/Tests/Functional/logs
|
||||
.idea
|
||||
|
@ -14,6 +14,7 @@ namespace Nelmio\ApiDocBundle\Annotation;
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
final class Areas
|
||||
{
|
||||
/** @var string[] */
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ use OpenApi\Annotations\Operation as BaseOperation;
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class Operation extends BaseOperation
|
||||
{
|
||||
}
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -67,12 +67,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, OA\AbstractAnnotation::class));
|
||||
$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, OA\AbstractAnnotation::class));
|
||||
|
||||
if (0 === count($annotations) && 0 === count($classAnnotations[$declaringClass->getName()])) {
|
||||
continue;
|
||||
@ -190,4 +192,23 @@ final class OpenApiPhpDescriber
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionClass|\ReflectionMethod $reflection
|
||||
*
|
||||
* @return OA\AbstractAnnotation[]
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
|
||||
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
|
||||
$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,
|
||||
@ -183,6 +183,20 @@ 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 (\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());
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -130,13 +130,23 @@ final class FilteredRouteCollectionBuilder
|
||||
}
|
||||
|
||||
/** @var Areas|null $areas */
|
||||
$areas = $this->annotationReader->getMethodAnnotation(
|
||||
$reflectionMethod,
|
||||
Areas::class
|
||||
);
|
||||
$areas = $this->getAttributesAsAnnotation($reflectionMethod, Areas::class)[0] ?? null;
|
||||
|
||||
if (null === $areas) {
|
||||
$areas = $this->annotationReader->getClassAnnotation($reflectionMethod->getDeclaringClass(), Areas::class);
|
||||
/** @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,
|
||||
Areas::class
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -11,244 +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\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;
|
||||
|
||||
/**
|
||||
* @Route("/api", name="api_", host="api.example.com")
|
||||
*/
|
||||
class ApiController
|
||||
{
|
||||
if (\PHP_VERSION_ID >= 80100) {
|
||||
/**
|
||||
* @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()
|
||||
class ApiController extends ApiController81
|
||||
{
|
||||
}
|
||||
|
||||
} else {
|
||||
/**
|
||||
* 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)")
|
||||
* )
|
||||
* @Route("/api", name="api_", host="api.example.com")
|
||||
*/
|
||||
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()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
251
Tests/Functional/Controller/ApiController80.php
Normal file
251
Tests/Functional/Controller/ApiController80.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?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\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("/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"})
|
||||
*
|
||||
* @Operation(operationId="custom-operation-id")
|
||||
* @OA\Response(response=200, description="success")
|
||||
*/
|
||||
public function customOperationIdAction()
|
||||
{
|
||||
}
|
||||
}
|
36
Tests/Functional/Controller/ApiController81.php
Normal file
36
Tests/Functional/Controller/ApiController81.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?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\Model;
|
||||
use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class ApiController81 extends ApiController80
|
||||
{
|
||||
#[OA\Get([
|
||||
'value' => new OA\Response(
|
||||
response: '200',
|
||||
description: 'Success',
|
||||
properties: [
|
||||
'value' => 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', properties: ['value' => new OA\Schema(type: 'string')])]
|
||||
public function fetchArticleActionWithAttributes()
|
||||
{
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* @Route("/api", host="api.example.com")
|
||||
*/
|
||||
class FOSRestController
|
||||
{
|
||||
if (\PHP_VERSION_ID >= 80100) {
|
||||
/**
|
||||
* @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 FOSRestController81
|
||||
{
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* @Route("/api", host="api.example.com")
|
||||
*/
|
||||
class FOSRestController extends FOSRestController80
|
||||
{
|
||||
}
|
||||
}
|
||||
|
37
Tests/Functional/Controller/FOSRestController80.php
Normal file
37
Tests/Functional/Controller/FOSRestController80.php
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
35
Tests/Functional/Controller/FOSRestController81.php
Normal file
35
Tests/Functional/Controller/FOSRestController81.php
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
@ -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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
67
Tests/ModelDescriber/Annotations/AnnotationReaderTest.php
Normal file
67
Tests/ModelDescriber/Annotations/AnnotationReaderTest.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?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\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() {
|
||||
#[OA\Property(example: 1)]
|
||||
private $property1;
|
||||
#[OA\Property(example: 'some example', description: 'some description')]
|
||||
private $property2;
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user