welcome back to dyb-tech
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
<?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\RouteDescriber;
|
||||
|
||||
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\Choice;
|
||||
use Symfony\Component\Validator\Constraints\DateTime;
|
||||
use Symfony\Component\Validator\Constraints\Regex;
|
||||
|
||||
final class FosRestDescriber implements RouteDescriberInterface
|
||||
{
|
||||
use RouteDescriberTrait;
|
||||
|
||||
/** @var Reader|null */
|
||||
private $annotationReader;
|
||||
|
||||
/** @var string[] */
|
||||
private $mediaTypes;
|
||||
|
||||
public function __construct(?Reader $annotationReader, array $mediaTypes)
|
||||
{
|
||||
$this->annotationReader = $annotationReader;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
}
|
||||
|
||||
public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
{
|
||||
$annotations = null !== $this->annotationReader
|
||||
? $this->annotationReader->getMethodAnnotations($reflectionMethod)
|
||||
: [];
|
||||
$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) {
|
||||
$parameterName = $annotation->key ?? $annotation->getName(); // the key used by fosrest
|
||||
|
||||
if ($annotation instanceof QueryParam) {
|
||||
$name = $parameterName.($annotation->map ? '[]' : '');
|
||||
$parameter = Util::getOperationParameter($operation, $name, 'query');
|
||||
$parameter->allowEmptyValue = $annotation->nullable && $annotation->allowBlank;
|
||||
|
||||
$parameter->required = !$annotation->nullable && $annotation->strict;
|
||||
|
||||
if (Generator::UNDEFINED === $parameter->description) {
|
||||
$parameter->description = $annotation->description;
|
||||
}
|
||||
|
||||
if ($annotation->map) {
|
||||
$parameter->explode = true;
|
||||
}
|
||||
|
||||
$schema = Util::getChild($parameter, OA\Schema::class);
|
||||
$this->describeCommonSchemaFromAnnotation($schema, $annotation);
|
||||
} else {
|
||||
/** @var OA\RequestBody $requestBody */
|
||||
$requestBody = Util::getChild($operation, OA\RequestBody::class);
|
||||
foreach ($this->mediaTypes as $mediaType) {
|
||||
$contentSchema = $this->getContentSchemaForType($requestBody, $mediaType);
|
||||
$schema = Util::getProperty($contentSchema, $parameterName);
|
||||
|
||||
if (!$annotation->nullable && $annotation->strict) {
|
||||
$requiredParameters = is_array($contentSchema->required) ? $contentSchema->required : [];
|
||||
$requiredParameters[] = $parameterName;
|
||||
|
||||
$contentSchema->required = array_values(array_unique($requiredParameters));
|
||||
}
|
||||
$this->describeCommonSchemaFromAnnotation($schema, $annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getPattern($requirements)
|
||||
{
|
||||
if (is_array($requirements) && isset($requirements['rule'])) {
|
||||
return (string) $requirements['rule'];
|
||||
}
|
||||
|
||||
if (is_string($requirements)) {
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
if ($requirements instanceof Regex) {
|
||||
return $requirements->getHtmlPattern();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getFormat($requirements)
|
||||
{
|
||||
if ($requirements instanceof Constraint && !$requirements instanceof Regex) {
|
||||
if ($requirements instanceof DateTime) {
|
||||
// As defined per RFC3339
|
||||
if (\DateTime::RFC3339 === $requirements->format || 'c' === $requirements->format) {
|
||||
return 'date-time';
|
||||
}
|
||||
|
||||
if ('Y-m-d' === $requirements->format) {
|
||||
return 'date';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$reflectionClass = new \ReflectionClass($requirements);
|
||||
|
||||
return $reflectionClass->getShortName();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getEnum($requirements)
|
||||
{
|
||||
if ($requirements instanceof Choice) {
|
||||
return $requirements->choices;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getContentSchemaForType(OA\RequestBody $requestBody, string $type): OA\Schema
|
||||
{
|
||||
$requestBody->content = Generator::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');
|
||||
}
|
||||
if (!isset($requestBody->content[$contentType])) {
|
||||
$weakContext = Util::createWeakContext($requestBody->_context);
|
||||
$requestBody->content[$contentType] = new OA\MediaType(
|
||||
[
|
||||
'mediaType' => $contentType,
|
||||
'_context' => $weakContext,
|
||||
]
|
||||
);
|
||||
/** @var OA\Schema $schema */
|
||||
$schema = Util::getChild(
|
||||
$requestBody->content[$contentType],
|
||||
OA\Schema::class
|
||||
);
|
||||
$schema->type = 'object';
|
||||
}
|
||||
|
||||
return Util::getChild(
|
||||
$requestBody->content[$contentType],
|
||||
OA\Schema::class
|
||||
);
|
||||
}
|
||||
|
||||
private function describeCommonSchemaFromAnnotation(OA\Schema $schema, $annotation)
|
||||
{
|
||||
$schema->default = $annotation->getDefault();
|
||||
|
||||
if (Generator::UNDEFINED === $schema->type) {
|
||||
$schema->type = $annotation->map ? 'array' : 'string';
|
||||
}
|
||||
|
||||
if ($annotation->map) {
|
||||
$schema->type = 'array';
|
||||
$schema->items = Util::getChild($schema, OA\Items::class);
|
||||
}
|
||||
|
||||
$pattern = $this->getPattern($annotation->requirements);
|
||||
if (null !== $pattern) {
|
||||
$schema->pattern = $pattern;
|
||||
}
|
||||
|
||||
$format = $this->getFormat($annotation->requirements);
|
||||
if (null !== $format) {
|
||||
$schema->format = $format;
|
||||
}
|
||||
|
||||
$enum = $this->getEnum($annotation->requirements);
|
||||
if (null !== $enum) {
|
||||
$schema->enum = $enum;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
@@ -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\RouteDescriber;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use phpDocumentor\Reflection\DocBlockFactoryInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
final class PhpDocDescriber implements RouteDescriberInterface
|
||||
{
|
||||
use RouteDescriberTrait;
|
||||
|
||||
private $docBlockFactory;
|
||||
|
||||
public function __construct(DocBlockFactoryInterface $docBlockFactory = null)
|
||||
{
|
||||
if (null === $docBlockFactory) {
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
}
|
||||
$this->docBlockFactory = $docBlockFactory;
|
||||
}
|
||||
|
||||
public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
{
|
||||
$classDocBlock = null;
|
||||
$docBlock = null;
|
||||
|
||||
try {
|
||||
$classDocBlock = $this->docBlockFactory->create($reflectionMethod->getDeclaringClass());
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$docBlock = $this->docBlockFactory->create($reflectionMethod);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
foreach ($this->getOperations($api, $route) as $operation) {
|
||||
if (null !== $docBlock) {
|
||||
if (Generator::UNDEFINED === $operation->summary && '' !== $docBlock->getSummary()) {
|
||||
$operation->summary = $docBlock->getSummary();
|
||||
}
|
||||
if (Generator::UNDEFINED === $operation->description && '' !== (string) $docBlock->getDescription()) {
|
||||
$operation->description = (string) $docBlock->getDescription();
|
||||
}
|
||||
if ($docBlock->hasTag('deprecated')) {
|
||||
$operation->deprecated = true;
|
||||
}
|
||||
}
|
||||
if (null !== $classDocBlock) {
|
||||
if ($classDocBlock->hasTag('deprecated')) {
|
||||
$operation->deprecated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber\RouteArgumentDescriberInterface;
|
||||
use OpenApi\Annotations as OA;
|
||||
use ReflectionMethod;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
final class RouteArgumentDescriber implements RouteDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
use RouteDescriberTrait;
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
/**
|
||||
* @param RouteArgumentDescriberInterface[] $inlineParameterDescribers
|
||||
*/
|
||||
public function __construct(
|
||||
private ArgumentMetadataFactoryInterface $argumentMetadataFactory,
|
||||
private iterable $inlineParameterDescribers
|
||||
) {
|
||||
}
|
||||
|
||||
public function describe(OA\OpenApi $api, Route $route, ReflectionMethod $reflectionMethod): void
|
||||
{
|
||||
$controller = $route->getDefault('_controller');
|
||||
|
||||
try {
|
||||
$argumentMetaDataList = $this->argumentMetadataFactory->createArgumentMetadata($controller, $reflectionMethod);
|
||||
} catch (\ReflectionException) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$argumentMetaDataList) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getOperations($api, $route) as $operation) {
|
||||
foreach ($argumentMetaDataList as $argumentMetadata) {
|
||||
foreach ($this->inlineParameterDescribers as $inlineParameterDescriber) {
|
||||
if ($inlineParameterDescriber instanceof ModelRegistryAwareInterface) {
|
||||
$inlineParameterDescriber->setModelRegistry($this->modelRegistry);
|
||||
}
|
||||
|
||||
$inlineParameterDescriber->describe($argumentMetadata, $operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
|
||||
interface RouteArgumentDescriberInterface
|
||||
{
|
||||
public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void;
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use OpenApi\Processors\Concerns\TypesTrait;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
|
||||
final class SymfonyMapQueryParameterDescriber implements RouteArgumentDescriberInterface
|
||||
{
|
||||
use TypesTrait;
|
||||
|
||||
public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void
|
||||
{
|
||||
/** @var MapQueryParameter $attribute */
|
||||
if (!$attribute = $argumentMetadata->getAttributes(MapQueryParameter::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $attribute->name ?? $argumentMetadata->getName();
|
||||
$name = 'array' === $argumentMetadata->getType()
|
||||
? $name.'[]'
|
||||
: $name;
|
||||
|
||||
$operationParameter = Util::getOperationParameter($operation, $name, 'query');
|
||||
|
||||
Util::modifyAnnotationValue($operationParameter, 'required', !($argumentMetadata->hasDefaultValue() || $argumentMetadata->isNullable()));
|
||||
|
||||
/** @var OA\Schema $schema */
|
||||
$schema = Util::getChild($operationParameter, OA\Schema::class);
|
||||
|
||||
if ($argumentMetadata->hasDefaultValue()) {
|
||||
Util::modifyAnnotationValue($schema, 'default', $argumentMetadata->getDefaultValue());
|
||||
}
|
||||
|
||||
if (Generator::UNDEFINED === $schema->nullable && $argumentMetadata->isNullable()) {
|
||||
Util::modifyAnnotationValue($schema, 'nullable', true);
|
||||
}
|
||||
|
||||
$defaultFilter = match ($argumentMetadata->getType()) {
|
||||
'array' => null,
|
||||
'string' => \FILTER_DEFAULT,
|
||||
'int' => \FILTER_VALIDATE_INT,
|
||||
'float' => \FILTER_VALIDATE_FLOAT,
|
||||
'bool' => \FILTER_VALIDATE_BOOL,
|
||||
default => null,
|
||||
};
|
||||
|
||||
$properties = $this->describeValidateFilter($attribute->filter ?? $defaultFilter, $attribute->flags, $attribute->options);
|
||||
|
||||
if ('array' === $argumentMetadata->getType()) {
|
||||
$schema->type = 'array';
|
||||
Util::getChild($schema, OA\Items::class, $properties);
|
||||
} else {
|
||||
foreach ($properties as $key => $value) {
|
||||
Util::modifyAnnotationValue($schema, $key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.php.net/manual/en/filter.filters.validate.php
|
||||
*/
|
||||
private function describeValidateFilter(?int $filter, int $flags, array $options): array
|
||||
{
|
||||
if (null === $filter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_BOOLEAN === $filter) {
|
||||
return ['type' => 'boolean'];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_DOMAIN === $filter) {
|
||||
return ['type' => 'string', 'format' => 'hostname'];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_EMAIL === $filter) {
|
||||
return ['type' => 'string', 'format' => 'email'];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_FLOAT === $filter) {
|
||||
return ['type' => 'number', 'format' => 'float'];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_INT === $filter) {
|
||||
$props = [];
|
||||
if ($options['min_range'] ?? false) {
|
||||
$props['minimum'] = $options['min_range'];
|
||||
}
|
||||
|
||||
if ($options['max_range'] ?? false) {
|
||||
$props['maximum'] = $options['max_range'];
|
||||
}
|
||||
|
||||
return ['type' => 'integer', ...$props];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_IP === $filter) {
|
||||
$format = match ($flags) {
|
||||
FILTER_FLAG_IPV4 => 'ipv4',
|
||||
FILTER_FLAG_IPV6 => 'ipv6',
|
||||
default => 'ip',
|
||||
};
|
||||
|
||||
return ['type' => 'string', 'format' => $format];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_MAC === $filter) {
|
||||
return ['type' => 'string', 'format' => 'mac'];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_REGEXP === $filter) {
|
||||
return ['type' => 'string', 'pattern' => $options['regexp']];
|
||||
}
|
||||
|
||||
if (FILTER_VALIDATE_URL === $filter) {
|
||||
return ['type' => 'string', 'format' => 'uri'];
|
||||
}
|
||||
|
||||
if (FILTER_DEFAULT === $filter) {
|
||||
return ['type' => 'string'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
|
||||
final class SymfonyMapQueryStringDescriber implements RouteArgumentDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
public const CONTEXT_KEY = 'nelmio_api_doc_bundle.map_query_string.'.self::class;
|
||||
public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class;
|
||||
public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class;
|
||||
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void
|
||||
{
|
||||
/** @var MapQueryString $attribute */
|
||||
if (!$attribute = $argumentMetadata->getAttributes(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modelRef = $this->modelRegistry->register(new Model(
|
||||
new Type(Type::BUILTIN_TYPE_OBJECT, $argumentMetadata->isNullable(), $argumentMetadata->getType()),
|
||||
groups: $this->getGroups($attribute),
|
||||
serializationContext: $attribute->serializationContext,
|
||||
));
|
||||
|
||||
if (!isset($operation->_context->{self::CONTEXT_KEY})) {
|
||||
$operation->_context->{self::CONTEXT_KEY} = [];
|
||||
}
|
||||
|
||||
$data = [
|
||||
self::CONTEXT_ARGUMENT_METADATA => $argumentMetadata,
|
||||
self::CONTEXT_MODEL_REF => $modelRef,
|
||||
];
|
||||
|
||||
$operation->_context->{self::CONTEXT_KEY}[] = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]|null
|
||||
*/
|
||||
private function getGroups(MapQueryString $attribute): ?array
|
||||
{
|
||||
if (null === $attribute->validationGroups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($attribute->validationGroups)) {
|
||||
return [$attribute->validationGroups];
|
||||
}
|
||||
|
||||
if (is_array($attribute->validationGroups)) {
|
||||
return $attribute->validationGroups;
|
||||
}
|
||||
|
||||
if ($attribute->validationGroups instanceof GroupSequence) {
|
||||
return $attribute->validationGroups->groups;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nelmio\ApiDocBundle\RouteDescriber\RouteArgumentDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||||
|
||||
final class SymfonyMapRequestPayloadDescriber implements RouteArgumentDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
public const CONTEXT_ARGUMENT_METADATA = 'nelmio_api_doc_bundle.argument_metadata.'.self::class;
|
||||
public const CONTEXT_MODEL_REF = 'nelmio_api_doc_bundle.model_ref.'.self::class;
|
||||
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
public function describe(ArgumentMetadata $argumentMetadata, OA\Operation $operation): void
|
||||
{
|
||||
/** @var MapRequestPayload $attribute */
|
||||
if (!$attribute = $argumentMetadata->getAttributes(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$modelRef = $this->modelRegistry->register(new Model(
|
||||
new Type(Type::BUILTIN_TYPE_OBJECT, false, $argumentMetadata->getType()),
|
||||
groups: $this->getGroups($attribute),
|
||||
serializationContext: $attribute->serializationContext,
|
||||
));
|
||||
|
||||
$operation->_context->{self::CONTEXT_ARGUMENT_METADATA} = $argumentMetadata;
|
||||
$operation->_context->{self::CONTEXT_MODEL_REF} = $modelRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]|null
|
||||
*/
|
||||
private function getGroups(MapRequestPayload $attribute): ?array
|
||||
{
|
||||
if (null === $attribute->validationGroups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($attribute->validationGroups)) {
|
||||
return [$attribute->validationGroups];
|
||||
}
|
||||
|
||||
if (is_array($attribute->validationGroups)) {
|
||||
return $attribute->validationGroups;
|
||||
}
|
||||
|
||||
if ($attribute->validationGroups instanceof GroupSequence) {
|
||||
return $attribute->validationGroups->groups;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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\RouteDescriber;
|
||||
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
interface RouteDescriberInterface
|
||||
{
|
||||
public function describe(OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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\RouteDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Annotations\OpenApi;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
trait RouteDescriberTrait
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return OA\Operation[]
|
||||
*/
|
||||
private function getOperations(OpenApi $api, Route $route): array
|
||||
{
|
||||
$operations = [];
|
||||
$path = Util::getPath($api, $this->normalizePath($route->getPath()));
|
||||
$methods = $route->getMethods() ?: Util::OPERATIONS;
|
||||
foreach ($methods as $method) {
|
||||
$method = strtolower($method);
|
||||
if (!in_array($method, Util::OPERATIONS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$operations[] = Util::getOperation($path, $method);
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
if ('.{_format}' === substr($path, -10)) {
|
||||
$path = substr($path, 0, -10);
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?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\RouteDescriber;
|
||||
|
||||
use LogicException;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Should be last route describer executed to make sure all params are set.
|
||||
*/
|
||||
final class RouteMetadataDescriber implements RouteDescriberInterface
|
||||
{
|
||||
use RouteDescriberTrait;
|
||||
|
||||
private const ALPHANUM_EXPANDED_REGEX = '/^[-a-zA-Z0-9_]*$/';
|
||||
|
||||
public function describe(OA\OpenApi $api, Route $route, \ReflectionMethod $reflectionMethod)
|
||||
{
|
||||
foreach ($this->getOperations($api, $route) as $operation) {
|
||||
$requirements = $route->getRequirements();
|
||||
$compiledRoute = $route->compile();
|
||||
$existingParams = $this->getRefParams($api, $operation);
|
||||
|
||||
// Don't include host requirements
|
||||
foreach ($compiledRoute->getPathVariables() as $pathVariable) {
|
||||
if ('_format' === $pathVariable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paramId = $pathVariable.'/path';
|
||||
/** @var OA\Parameter $parameter */
|
||||
$parameter = $existingParams[$paramId] ?? null;
|
||||
if (null !== $parameter) {
|
||||
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()));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameter = Util::getOperationParameter($operation, $pathVariable, 'path');
|
||||
$parameter->required = true;
|
||||
|
||||
$parameter->schema = Util::getChild($parameter, OA\Schema::class);
|
||||
|
||||
if (Generator::UNDEFINED === $parameter->schema->type) {
|
||||
$parameter->schema->type = 'string';
|
||||
}
|
||||
|
||||
// handle pattern and possible enum values
|
||||
if (isset($requirements[$pathVariable])) {
|
||||
$req = $requirements[$pathVariable];
|
||||
$enumValues = $this->getPossibleEnumValues($req);
|
||||
if ($enumValues && Generator::UNDEFINED === $parameter->schema->pattern) {
|
||||
$parameter->schema->enum = $enumValues;
|
||||
}
|
||||
// add the pattern anyway
|
||||
if (Generator::UNDEFINED === $parameter->schema->pattern) {
|
||||
$parameter->schema->pattern = $requirements[$pathVariable];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The '$ref' parameters need special handling, since their objects are missing 'name' and 'in'.
|
||||
*
|
||||
* @return OA\Parameter[] existing $ref parameters
|
||||
*/
|
||||
private function getRefParams(OA\OpenApi $api, OA\Operation $operation): array
|
||||
{
|
||||
/** @var OA\Parameter[] $globalParams */
|
||||
$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 = Generator::UNDEFINED !== $operation->parameters ? $operation->parameters : [];
|
||||
/** @var OA\Parameter $parameter */
|
||||
foreach ($operationParameters as $id => $parameter) {
|
||||
$ref = $parameter->ref;
|
||||
if (Generator::UNDEFINED === $ref) {
|
||||
// we only concern ourselves with '$ref' parameters, so continue the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
$ref = \mb_substr($ref, 24); // trim the '#/components/parameters/' part of ref
|
||||
if (!isset($globalParams[$ref])) {
|
||||
// this shouldn't happen during proper configs, but in case of bad config, just ignore it here
|
||||
continue;
|
||||
}
|
||||
|
||||
$refParameter = $globalParams[$ref];
|
||||
|
||||
// param ids are in form {name}/{in}
|
||||
$existingParams[\sprintf('%s/%s', $refParameter->name, $refParameter->in)] = $refParameter;
|
||||
}
|
||||
|
||||
return $existingParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns array of separated alphanumeric (including '-', '_') strings from a simple OR regex requirement pattern.
|
||||
* (routing parameters containing enums have already been resolved to that format at this time).
|
||||
*
|
||||
* @param string $reqPattern a requirement pattern to match, e.g. 'a.html|b.html'
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private function getPossibleEnumValues(string $reqPattern): array
|
||||
{
|
||||
$requirements = [];
|
||||
if (false !== strpos($reqPattern, '|')) {
|
||||
$parts = explode('|', $reqPattern);
|
||||
foreach ($parts as $part) {
|
||||
if ('' === $part || 0 === preg_match(self::ALPHANUM_EXPANDED_REGEX, $part)) {
|
||||
// we check a complex regex expression containing | - abort in that case
|
||||
return [];
|
||||
} else {
|
||||
$requirements[] = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user