welcome back to dyb-tech
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
||||
<?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\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AnnotationsReader
|
||||
{
|
||||
private $phpDocReader;
|
||||
private $openApiAnnotationsReader;
|
||||
private $symfonyConstraintAnnotationReader;
|
||||
|
||||
public function __construct(
|
||||
?Reader $annotationsReader,
|
||||
ModelRegistry $modelRegistry,
|
||||
array $mediaTypes,
|
||||
bool $useValidationGroups = false
|
||||
) {
|
||||
$this->phpDocReader = new PropertyPhpDocReader();
|
||||
$this->openApiAnnotationsReader = new OpenApiAnnotationsReader($annotationsReader, $modelRegistry, $mediaTypes);
|
||||
$this->symfonyConstraintAnnotationReader = new SymfonyConstraintAnnotationReader(
|
||||
$annotationsReader,
|
||||
$useValidationGroups
|
||||
);
|
||||
}
|
||||
|
||||
public function updateDefinition(\ReflectionClass $reflectionClass, OA\Schema $schema): UpdateClassDefinitionResult
|
||||
{
|
||||
$this->openApiAnnotationsReader->updateSchema($reflectionClass, $schema);
|
||||
$this->symfonyConstraintAnnotationReader->setSchema($schema);
|
||||
|
||||
return new UpdateClassDefinitionResult(
|
||||
$this->shouldDescribeModelProperties($schema)
|
||||
);
|
||||
}
|
||||
|
||||
public function getPropertyName($reflection, string $default): string
|
||||
{
|
||||
return $this->openApiAnnotationsReader->getPropertyName($reflection, $default);
|
||||
}
|
||||
|
||||
public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void
|
||||
{
|
||||
$this->openApiAnnotationsReader->updateProperty($reflection, $property, $serializationGroups);
|
||||
$this->phpDocReader->updateProperty($reflection, $property);
|
||||
$this->symfonyConstraintAnnotationReader->updateProperty($reflection, $property, $serializationGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* if an objects schema type and ref are undefined OR the object was manually
|
||||
* defined as an object, then we're good to do the normal describe flow of
|
||||
* class properties.
|
||||
*/
|
||||
private function shouldDescribeModelProperties(OA\Schema $schema): bool
|
||||
{
|
||||
return (Generator::UNDEFINED === $schema->type || 'object' === $schema->type)
|
||||
&& Generator::UNDEFINED === $schema->ref;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?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\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use Nelmio\ApiDocBundle\Util\SetsContextTrait;
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class OpenApiAnnotationsReader
|
||||
{
|
||||
use SetsContextTrait;
|
||||
|
||||
/**
|
||||
* @var Reader|null
|
||||
*/
|
||||
private $annotationsReader;
|
||||
private $modelRegister;
|
||||
|
||||
public function __construct(?Reader $annotationsReader, ModelRegistry $modelRegistry, array $mediaTypes)
|
||||
{
|
||||
$this->annotationsReader = $annotationsReader;
|
||||
$this->modelRegister = new ModelRegister($modelRegistry, $mediaTypes);
|
||||
}
|
||||
|
||||
public function updateSchema(\ReflectionClass $reflectionClass, OA\Schema $schema): void
|
||||
{
|
||||
/** @var OA\Schema|null $oaSchema */
|
||||
if (!$oaSchema = $this->getAnnotation($schema->_context, $reflectionClass, OA\Schema::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read @Model annotations
|
||||
$this->modelRegister->__invoke(new Analysis([$oaSchema], Util::createContext()));
|
||||
|
||||
if (!$oaSchema->validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->mergeProperties($oaSchema);
|
||||
}
|
||||
|
||||
public function getPropertyName($reflection, string $default): string
|
||||
{
|
||||
/** @var OA\Property|null $oaProperty */
|
||||
if (!$oaProperty = $this->getAnnotation(new Context(), $reflection, OA\Property::class)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return Generator::UNDEFINED !== $oaProperty->property ? $oaProperty->property : $default;
|
||||
}
|
||||
|
||||
public function updateProperty($reflection, OA\Property $property, array $serializationGroups = null): void
|
||||
{
|
||||
/** @var OA\Property|null $oaProperty */
|
||||
if (!$oaProperty = $this->getAnnotation($property->_context, $reflection, OA\Property::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read @Model annotations
|
||||
$this->modelRegister->__invoke(new Analysis([$oaProperty], Util::createContext()), $serializationGroups);
|
||||
|
||||
if (!$oaProperty->validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property->mergeProperties($oaProperty);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionClass|\ReflectionProperty|\ReflectionMethod $reflection
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function getAnnotation(Context $parentContext, $reflection, string $className)
|
||||
{
|
||||
$this->setContextFromReflection($parentContext, $reflection);
|
||||
|
||||
try {
|
||||
if (\PHP_VERSION_ID >= 80100) {
|
||||
if (null !== $attribute = $reflection->getAttributes($className, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
|
||||
return $attribute->newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $this->annotationsReader) {
|
||||
if ($reflection instanceof \ReflectionClass) {
|
||||
return $this->annotationsReader->getClassAnnotation($reflection, $className);
|
||||
} elseif ($reflection instanceof \ReflectionProperty) {
|
||||
return $this->annotationsReader->getPropertyAnnotation($reflection, $className);
|
||||
} elseif ($reflection instanceof \ReflectionMethod) {
|
||||
return $this->annotationsReader->getMethodAnnotation($reflection, $className);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$this->setContext(null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?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\ModelDescriber\Annotations;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
|
||||
/**
|
||||
* Extract information about properties of a model from the DocBlock comment.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PropertyPhpDocReader
|
||||
{
|
||||
private $docBlockFactory;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the Swagger information with information from the DocBlock comment.
|
||||
*/
|
||||
public function updateProperty($reflection, OA\Property $property): void
|
||||
{
|
||||
try {
|
||||
$docBlock = $this->docBlockFactory->create($reflection);
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$title = $docBlock->getSummary()) {
|
||||
/** @var Var_ $var */
|
||||
foreach ($docBlock->getTagsByName('var') as $var) {
|
||||
if (!method_exists($var, 'getDescription') || !$description = $var->getDescription()) {
|
||||
continue;
|
||||
}
|
||||
$title = $description->render();
|
||||
if ($title) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Generator::UNDEFINED === $property->title && $title) {
|
||||
$property->title = $title;
|
||||
}
|
||||
if (Generator::UNDEFINED === $property->description && $docBlock->getDescription() && $docBlock->getDescription()->render()) {
|
||||
$property->description = $docBlock->getDescription()->render();
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+233
@@ -0,0 +1,233 @@
|
||||
<?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\ModelDescriber\Annotations;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use Nelmio\ApiDocBundle\Util\SetsContextTrait;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SymfonyConstraintAnnotationReader
|
||||
{
|
||||
use SetsContextTrait;
|
||||
|
||||
/**
|
||||
* @var Reader|null
|
||||
*/
|
||||
private $annotationsReader;
|
||||
|
||||
/**
|
||||
* @var OA\Schema
|
||||
*/
|
||||
private $schema;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $useValidationGroups;
|
||||
|
||||
public function __construct(?Reader $annotationsReader, bool $useValidationGroups = false)
|
||||
{
|
||||
$this->annotationsReader = $annotationsReader;
|
||||
$this->useValidationGroups = $useValidationGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given property and schema with defined Symfony constraints.
|
||||
*
|
||||
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
||||
*/
|
||||
public function updateProperty($reflection, OA\Property $property, ?array $validationGroups = null): void
|
||||
{
|
||||
foreach ($this->getAnnotations($property->_context, $reflection, $validationGroups) as $outerAnnotation) {
|
||||
$innerAnnotations = $outerAnnotation instanceof Assert\Compound || $outerAnnotation instanceof Assert\Sequentially
|
||||
? $outerAnnotation->constraints
|
||||
: [$outerAnnotation];
|
||||
|
||||
$this->processPropertyAnnotations($reflection, $property, $innerAnnotations);
|
||||
}
|
||||
}
|
||||
|
||||
private function processPropertyAnnotations($reflection, OA\Property $property, $annotations)
|
||||
{
|
||||
foreach ($annotations as $annotation) {
|
||||
if ($annotation instanceof Assert\NotBlank || $annotation instanceof Assert\NotNull) {
|
||||
// To support symfony/validator < 4.3
|
||||
if ($annotation instanceof Assert\NotBlank && \property_exists($annotation, 'allowNull') && $annotation->allowNull) {
|
||||
// The field is optional
|
||||
return;
|
||||
}
|
||||
|
||||
// The field is required
|
||||
if (null === $this->schema) {
|
||||
return;
|
||||
}
|
||||
|
||||
$propertyName = Util::getSchemaPropertyName($this->schema, $property);
|
||||
if (null === $propertyName) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existingRequiredFields = Generator::UNDEFINED !== $this->schema->required ? $this->schema->required : [];
|
||||
$existingRequiredFields[] = $propertyName;
|
||||
|
||||
$this->schema->required = array_values(array_unique($existingRequiredFields));
|
||||
} elseif ($annotation instanceof Assert\Length) {
|
||||
if (isset($annotation->min)) {
|
||||
$property->minLength = (int) $annotation->min;
|
||||
}
|
||||
if (isset($annotation->max)) {
|
||||
$property->maxLength = (int) $annotation->max;
|
||||
}
|
||||
} elseif ($annotation instanceof Assert\Regex) {
|
||||
$this->appendPattern($property, $annotation->getHtmlPattern());
|
||||
} elseif ($annotation instanceof Assert\Count) {
|
||||
if (isset($annotation->min)) {
|
||||
$property->minItems = (int) $annotation->min;
|
||||
}
|
||||
if (isset($annotation->max)) {
|
||||
$property->maxItems = (int) $annotation->max;
|
||||
}
|
||||
} elseif ($annotation instanceof Assert\Choice) {
|
||||
$this->applyEnumFromChoiceConstraint($property, $annotation, $reflection);
|
||||
} elseif ($annotation instanceof Assert\Range) {
|
||||
if (\is_int($annotation->min)) {
|
||||
$property->minimum = $annotation->min;
|
||||
}
|
||||
if (\is_int($annotation->max)) {
|
||||
$property->maximum = $annotation->max;
|
||||
}
|
||||
} elseif ($annotation instanceof Assert\LessThan) {
|
||||
if (\is_int($annotation->value)) {
|
||||
$property->exclusiveMaximum = true;
|
||||
$property->maximum = $annotation->value;
|
||||
}
|
||||
} elseif ($annotation instanceof Assert\LessThanOrEqual) {
|
||||
if (\is_int($annotation->value)) {
|
||||
$property->maximum = $annotation->value;
|
||||
}
|
||||
} elseif ($annotation instanceof Assert\GreaterThan) {
|
||||
if (\is_int($annotation->value)) {
|
||||
$property->exclusiveMinimum = true;
|
||||
$property->minimum = $annotation->value;
|
||||
}
|
||||
} elseif ($annotation instanceof Assert\GreaterThanOrEqual) {
|
||||
if (\is_int($annotation->value)) {
|
||||
$property->minimum = $annotation->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setSchema($schema): void
|
||||
{
|
||||
$this->schema = $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the pattern from the constraint to the existing pattern.
|
||||
*/
|
||||
private function appendPattern(OA\Schema $property, $newPattern): void
|
||||
{
|
||||
if (null === $newPattern) {
|
||||
return;
|
||||
}
|
||||
if (Generator::UNDEFINED !== $property->pattern) {
|
||||
$property->pattern = sprintf('%s, %s', $property->pattern, $newPattern);
|
||||
} else {
|
||||
$property->pattern = $newPattern;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
||||
*/
|
||||
private function applyEnumFromChoiceConstraint(OA\Schema $property, Assert\Choice $choice, $reflection): void
|
||||
{
|
||||
if ($choice->callback) {
|
||||
$enumValues = call_user_func(is_array($choice->callback) ? $choice->callback : [$reflection->class, $choice->callback]);
|
||||
} else {
|
||||
$enumValues = $choice->choices;
|
||||
}
|
||||
|
||||
$setEnumOnThis = $property;
|
||||
if ($choice->multiple) {
|
||||
$setEnumOnThis = Util::getChild($property, OA\Items::class);
|
||||
}
|
||||
|
||||
$setEnumOnThis->enum = array_values($enumValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
||||
*/
|
||||
private function getAnnotations(Context $parentContext, $reflection, ?array $validationGroups): iterable
|
||||
{
|
||||
// To correctly load OA annotations
|
||||
$this->setContextFromReflection($parentContext, $reflection);
|
||||
|
||||
foreach ($this->locateAnnotations($reflection) as $annotation) {
|
||||
if (!$annotation instanceof Constraint) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->useValidationGroups || $this->isConstraintInGroup($annotation, $validationGroups)) {
|
||||
yield $annotation;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setContext(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionProperty|\ReflectionMethod $reflection
|
||||
*/
|
||||
private function locateAnnotations($reflection): \Traversable
|
||||
{
|
||||
if (\PHP_VERSION_ID >= 80000 && class_exists(Constraint::class)) {
|
||||
foreach ($reflection->getAttributes(Constraint::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
|
||||
yield $attribute->newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $this->annotationsReader) {
|
||||
if ($reflection instanceof \ReflectionProperty) {
|
||||
yield from $this->annotationsReader->getPropertyAnnotations($reflection);
|
||||
} elseif ($reflection instanceof \ReflectionMethod) {
|
||||
yield from $this->annotationsReader->getMethodAnnotations($reflection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the given constraint is in the provided serialization groups.
|
||||
*
|
||||
* If no groups are provided the validator would run in the Constraint::DEFAULT_GROUP,
|
||||
* and constraints without any `groups` passed to them would be in that same
|
||||
* default group. So even with a null $validationGroups passed here there still
|
||||
* has to be a check on the default group.
|
||||
*/
|
||||
private function isConstraintInGroup(Constraint $annotation, ?array $validationGroups): bool
|
||||
{
|
||||
return count(array_intersect(
|
||||
$validationGroups ?: [Constraint::DEFAULT_GROUP],
|
||||
(array) $annotation->groups
|
||||
)) > 0;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?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\ModelDescriber\Annotations;
|
||||
|
||||
/**
|
||||
* result object returned from `AnnotationReader::updateDefinition` as a way
|
||||
* to pass back information about manually defined schema elements.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class UpdateClassDefinitionResult
|
||||
{
|
||||
/**
|
||||
* Whether or not the model describer shoudl continue reading class properties
|
||||
* after updating the open api schema from an `OA\Schema` definition.
|
||||
*
|
||||
* Users may maually define a `type` or `ref` on a schema, and if that's the case
|
||||
* model describers should _probably_ not describe any additional properties or try
|
||||
* to merge in properties.
|
||||
*/
|
||||
private $shouldDescribeModelProperties;
|
||||
|
||||
public function __construct(bool $shouldDescribeModelProperties)
|
||||
{
|
||||
$this->shouldDescribeModelProperties = $shouldDescribeModelProperties;
|
||||
}
|
||||
|
||||
public function shouldDescribeModelProperties(): bool
|
||||
{
|
||||
return $this->shouldDescribeModelProperties;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Contains helper methods that add `discriminator` and `oneOf` values to
|
||||
* Open API schemas to support poly morphism.
|
||||
*
|
||||
* @see https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait ApplyOpenApiDiscriminatorTrait
|
||||
{
|
||||
/**
|
||||
* @param Model $model the model that's being described, This is used to pass groups and config
|
||||
* down to the children models in `oneOf`
|
||||
* @param OA\Schema $schema The Open API schema to which `oneOf` and `discriminator` properties
|
||||
* will be added
|
||||
* @param string $discriminatorProperty The property that determine which model will be unsierailized
|
||||
* @param array<string, string> $typeMap the map of $discriminatorProperty values to their
|
||||
* types
|
||||
*/
|
||||
protected function applyOpenApiDiscriminator(
|
||||
Model $model,
|
||||
OA\Schema $schema,
|
||||
ModelRegistry $modelRegistry,
|
||||
string $discriminatorProperty,
|
||||
array $typeMap
|
||||
): void {
|
||||
$weakContext = Util::createWeakContext($schema->_context);
|
||||
|
||||
$schema->oneOf = [];
|
||||
$schema->discriminator = new OA\Discriminator(['_context' => $weakContext]);
|
||||
$schema->discriminator->propertyName = $discriminatorProperty;
|
||||
$schema->discriminator->mapping = [];
|
||||
foreach ($typeMap as $propertyValue => $className) {
|
||||
$oneOfSchema = new OA\Schema(['_context' => $weakContext]);
|
||||
$oneOfSchema->ref = $modelRegistry->register(new Model(
|
||||
new Type(Type::BUILTIN_TYPE_OBJECT, false, $className),
|
||||
$model->getGroups(),
|
||||
$model->getOptions()
|
||||
));
|
||||
$schema->oneOf[] = $oneOfSchema;
|
||||
$schema->discriminator->mapping[$propertyValue] = $oneOfSchema->ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Hateoas\Configuration\Metadata\ClassMetadata;
|
||||
use Hateoas\Configuration\Relation;
|
||||
use Hateoas\Serializer\Metadata\RelationPropertyMetadata;
|
||||
use Metadata\MetadataFactoryInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\Model\ModelRegistry;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
private $factory;
|
||||
private $JMSModelDescriber;
|
||||
|
||||
public function __construct(MetadataFactoryInterface $factory, JMSModelDescriber $JMSModelDescriber)
|
||||
{
|
||||
$this->factory = $factory;
|
||||
$this->JMSModelDescriber = $JMSModelDescriber;
|
||||
}
|
||||
|
||||
public function setModelRegistry(ModelRegistry $modelRegistry)
|
||||
{
|
||||
$this->modelRegistry = $modelRegistry;
|
||||
$this->JMSModelDescriber->setModelRegistry($modelRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function describe(Model $model, OA\Schema $schema): void
|
||||
{
|
||||
$this->JMSModelDescriber->describe($model, $schema);
|
||||
|
||||
/**
|
||||
* @var ClassMetadata
|
||||
*/
|
||||
$metadata = $this->getHateoasMetadata($model);
|
||||
if (null === $metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->type = 'object';
|
||||
$context = $this->JMSModelDescriber->getSerializationContext($model);
|
||||
|
||||
/** @var Relation $relation */
|
||||
foreach ($metadata->getRelations() as $relation) {
|
||||
if (!$relation->getEmbedded() && !$relation->getHref()) {
|
||||
continue;
|
||||
}
|
||||
$item = new RelationPropertyMetadata($relation->getExclusion(), $relation);
|
||||
|
||||
if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->pushPropertyMetadata($item);
|
||||
|
||||
$embedded = $relation->getEmbedded();
|
||||
$relationSchema = Util::getProperty($schema, $relation->getEmbedded() ? '_embedded' : '_links');
|
||||
$relationSchema->readOnly = true;
|
||||
|
||||
$property = Util::getProperty($relationSchema, $relation->getName());
|
||||
if ($embedded && method_exists($embedded, 'getType') && $embedded->getType()) {
|
||||
$this->JMSModelDescriber->describeItem($embedded->getType(), $property, $context);
|
||||
} else {
|
||||
$property->type = 'object';
|
||||
}
|
||||
if ($relation->getHref()) {
|
||||
$hrefProp = Util::getProperty($property, 'href');
|
||||
$hrefProp->type = 'string';
|
||||
$this->setAttributeProperties($relation, $property);
|
||||
}
|
||||
|
||||
$context->popPropertyMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
private function getHateoasMetadata(Model $model)
|
||||
{
|
||||
$className = $model->getType()->getClassName();
|
||||
|
||||
try {
|
||||
if ($metadata = $this->factory->getMetadataForClass($className)) {
|
||||
return $metadata;
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return $this->JMSModelDescriber->supports($model) || null !== $this->getHateoasMetadata($model);
|
||||
}
|
||||
|
||||
private function setAttributeProperties(Relation $relation, OA\Property $subProperty): void
|
||||
{
|
||||
foreach ($relation->getAttributes() as $attribute => $value) {
|
||||
$subSubProp = Util::getProperty($subProperty, $attribute);
|
||||
switch (gettype($value)) {
|
||||
case 'integer':
|
||||
$subSubProp->type = 'integer';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
case 'double':
|
||||
case 'float':
|
||||
$subSubProp->type = 'number';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
case 'boolean':
|
||||
$subSubProp->type = 'boolean';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
case 'string':
|
||||
$subSubProp->type = 'string';
|
||||
$subSubProp->default = $value;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Nelmio\ApiDocBundle\ModelDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations\Schema;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class EnumModelDescriber implements ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, Schema $schema)
|
||||
{
|
||||
$enumClass = $model->getType()->getClassName();
|
||||
|
||||
$enums = [];
|
||||
foreach ($enumClass::cases() as $enumCase) {
|
||||
$enums[] = $enumCase->value;
|
||||
}
|
||||
|
||||
$reflectionEnum = new \ReflectionEnum($enumClass);
|
||||
if ($reflectionEnum->isBacked() && 'int' === $reflectionEnum->getBackingType()->getName()) {
|
||||
$schema->type = 'integer';
|
||||
} else {
|
||||
$schema->type = 'string';
|
||||
}
|
||||
$schema->enum = $enums;
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
if (!function_exists('enum_exists')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType()
|
||||
&& enum_exists($model->getType()->getClassName())
|
||||
&& is_subclass_of($model->getType()->getClassName(), \BackedEnum::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
class FallbackObjectModelDescriber implements ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use Nelmio\ApiDocBundle\Util\SetsContextTrait;
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormConfigInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormTypeInterface;
|
||||
use Symfony\Component\Form\ResolvedFormTypeInterface;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class FormModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
use SetsContextTrait;
|
||||
|
||||
private $formFactory;
|
||||
|
||||
/**
|
||||
* @var Reader|null
|
||||
*/
|
||||
private $doctrineReader;
|
||||
private $mediaTypes;
|
||||
private $useValidationGroups;
|
||||
private $isFormCsrfExtensionEnabled;
|
||||
|
||||
public function __construct(
|
||||
FormFactoryInterface $formFactory = null,
|
||||
Reader $reader = null,
|
||||
array $mediaTypes = null,
|
||||
bool $useValidationGroups = false,
|
||||
bool $isFormCsrfExtensionEnabled = false
|
||||
) {
|
||||
$this->formFactory = $formFactory;
|
||||
$this->doctrineReader = $reader;
|
||||
|
||||
if (null === $mediaTypes) {
|
||||
$mediaTypes = ['json'];
|
||||
|
||||
trigger_deprecation('nelmio/api-doc-bundle', '4.1', 'Not passing media types to the constructor of %s is deprecated and won\'t be allowed in version 5.', self::class);
|
||||
}
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
$this->useValidationGroups = $useValidationGroups;
|
||||
$this->isFormCsrfExtensionEnabled = $isFormCsrfExtensionEnabled;
|
||||
}
|
||||
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
if (method_exists(AbstractType::class, 'setDefaultOptions')) {
|
||||
throw new \LogicException('symfony/form < 3.0 is not supported, please upgrade to an higher version to use a form as a model.');
|
||||
}
|
||||
if (null === $this->formFactory) {
|
||||
throw new \LogicException('You need to enable forms in your application to use a form as a model.');
|
||||
}
|
||||
|
||||
$class = $model->getType()->getClassName();
|
||||
|
||||
$annotationsReader = new AnnotationsReader(
|
||||
$this->doctrineReader,
|
||||
$this->modelRegistry,
|
||||
$this->mediaTypes,
|
||||
$this->useValidationGroups
|
||||
);
|
||||
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($class), $schema);
|
||||
|
||||
if (!$classResult->shouldDescribeModelProperties()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->type = 'object';
|
||||
|
||||
$this->setContextFromReflection($schema->_context, new \ReflectionClass($class));
|
||||
|
||||
$form = $this->formFactory->create($class, null, $model->getOptions() ?? []);
|
||||
$this->parseForm($schema, $form);
|
||||
|
||||
$this->setContext(null);
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return is_a($model->getType()->getClassName(), FormTypeInterface::class, true);
|
||||
}
|
||||
|
||||
private function parseForm(OA\Schema $schema, FormInterface $form)
|
||||
{
|
||||
foreach ($form as $name => $child) {
|
||||
$config = $child->getConfig();
|
||||
|
||||
// This field must not be documented
|
||||
if ($config->hasOption('documentation') && false === $config->getOption('documentation')) {
|
||||
continue;
|
||||
}
|
||||
$property = Util::getProperty($schema, $name);
|
||||
|
||||
if ($config->getRequired()) {
|
||||
$required = Generator::UNDEFINED !== $schema->required ? $schema->required : [];
|
||||
$required[] = $name;
|
||||
|
||||
$schema->required = $required;
|
||||
}
|
||||
|
||||
if ($config->hasOption('documentation')) {
|
||||
$property->mergeProperties($config->getOption('documentation'));
|
||||
|
||||
// Parse inner @Model annotations
|
||||
$modelRegister = new ModelRegister($this->modelRegistry, $this->mediaTypes);
|
||||
$modelRegister->__invoke(new Analysis([$property], Util::createContext()));
|
||||
}
|
||||
|
||||
if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
|
||||
continue; // Type manually defined
|
||||
}
|
||||
|
||||
$this->findFormType($config, $property);
|
||||
}
|
||||
|
||||
if ($this->isFormCsrfExtensionEnabled && $form->getConfig()->getOption('csrf_protection', false)) {
|
||||
$tokenFieldName = $form->getConfig()->getOption('csrf_field_name');
|
||||
|
||||
$property = Util::getProperty($schema, $tokenFieldName);
|
||||
$property->type = 'string';
|
||||
$property->description = 'CSRF token';
|
||||
|
||||
if (Generator::isDefault($schema->required)) {
|
||||
$schema->required = [];
|
||||
}
|
||||
|
||||
$schema->required[] = $tokenFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and sets the schema type on $property based on $config info.
|
||||
*
|
||||
* Returns true if a native OpenAPi type was found, false otherwise
|
||||
*/
|
||||
private function findFormType(FormConfigInterface $config, OA\Schema $property)
|
||||
{
|
||||
$type = $config->getType();
|
||||
|
||||
if (!$builtinFormType = $this->getBuiltinFormType($type)) {
|
||||
// if form type is not builtin in Form component.
|
||||
$model = new Model(
|
||||
new Type(Type::BUILTIN_TYPE_OBJECT, false, get_class($type->getInnerType())),
|
||||
null,
|
||||
$config->getOptions()
|
||||
);
|
||||
|
||||
$ref = $this->modelRegistry->register($model);
|
||||
// We need to use allOf for description and title to be displayed
|
||||
if ($config->hasOption('documentation') && !empty($config->getOption('documentation'))) {
|
||||
$property->oneOf = [new OA\Schema(['ref' => $ref])];
|
||||
} else {
|
||||
$property->ref = $ref;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
$blockPrefix = $builtinFormType->getBlockPrefix();
|
||||
|
||||
if ('text' === $blockPrefix) {
|
||||
$property->type = 'string';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('number' === $blockPrefix) {
|
||||
$property->type = 'number';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('integer' === $blockPrefix) {
|
||||
$property->type = 'integer';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('date' === $blockPrefix) {
|
||||
$property->type = 'string';
|
||||
$property->format = 'date';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('datetime' === $blockPrefix) {
|
||||
$property->type = 'string';
|
||||
$property->format = 'date-time';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('choice' === $blockPrefix) {
|
||||
if ($config->getOption('multiple')) {
|
||||
$property->type = 'array';
|
||||
} else {
|
||||
$property->type = 'string';
|
||||
}
|
||||
if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) {
|
||||
$enums = array_values($choices);
|
||||
if ($this->isNumbersArray($enums)) {
|
||||
$type = 'number';
|
||||
} elseif ($this->isBooleansArray($enums)) {
|
||||
$type = 'boolean';
|
||||
} else {
|
||||
$type = 'string';
|
||||
}
|
||||
|
||||
if ($config->getOption('multiple')) {
|
||||
$property->items = Util::createChild($property, OA\Items::class, ['type' => $type, 'enum' => $enums]);
|
||||
} else {
|
||||
$property->type = $type;
|
||||
$property->enum = $enums;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('checkbox' === $blockPrefix) {
|
||||
$property->type = 'boolean';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('password' === $blockPrefix) {
|
||||
$property->type = 'string';
|
||||
$property->format = 'password';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('repeated' === $blockPrefix) {
|
||||
$property->type = 'object';
|
||||
$property->required = [$config->getOption('first_name'), $config->getOption('second_name')];
|
||||
$subType = $config->getOption('type');
|
||||
|
||||
foreach (['first', 'second'] as $subField) {
|
||||
$subName = $config->getOption($subField.'_name');
|
||||
$subForm = $this->formFactory->create($subType, null, array_merge($config->getOption('options'), $config->getOption($subField.'_options')));
|
||||
|
||||
$this->findFormType($subForm->getConfig(), Util::getProperty($property, $subName));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ('collection' === $blockPrefix) {
|
||||
$subType = $config->getOption('entry_type');
|
||||
$subOptions = $config->getOption('entry_options');
|
||||
$subForm = $this->formFactory->create($subType, null, $subOptions);
|
||||
|
||||
$property->type = 'array';
|
||||
$property->items = Util::createChild($property, OA\Items::class);
|
||||
|
||||
$this->findFormType($subForm->getConfig(), $property->items);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// The DocumentType is bundled with the DoctrineMongoDBBundle
|
||||
if ('entity' === $blockPrefix || 'document' === $blockPrefix) {
|
||||
$entityClass = $config->getOption('class');
|
||||
|
||||
if ($config->getOption('multiple')) {
|
||||
$property->format = sprintf('[%s id]', $entityClass);
|
||||
$property->type = 'array';
|
||||
$property->items = Util::createChild($property, OA\Items::class, ['type' => 'string']);
|
||||
} else {
|
||||
$property->type = 'string';
|
||||
$property->format = sprintf('%s id', $entityClass);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
} while ($builtinFormType = $builtinFormType->getParent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if $array contains only numbers, false otherwise
|
||||
*/
|
||||
private function isNumbersArray(array $array): bool
|
||||
{
|
||||
foreach ($array as $item) {
|
||||
if (!is_numeric($item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if $array contains only booleans, false otherwise
|
||||
*/
|
||||
private function isBooleansArray(array $array): bool
|
||||
{
|
||||
foreach ($array as $item) {
|
||||
if (!is_bool($item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ResolvedFormTypeInterface|null
|
||||
*/
|
||||
private function getBuiltinFormType(ResolvedFormTypeInterface $type)
|
||||
{
|
||||
do {
|
||||
$class = get_class($type->getInnerType());
|
||||
|
||||
if (FormType::class === $class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('entity' === $type->getBlockPrefix() || 'document' === $type->getBlockPrefix()) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (0 === strpos($class, 'Symfony\Component\Form\Extension\Core\Type\\')) {
|
||||
return $type;
|
||||
}
|
||||
} while ($type = $type->getParent());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use JMS\Serializer\Context;
|
||||
use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
|
||||
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
|
||||
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
|
||||
use JMS\Serializer\SerializationContext;
|
||||
use Metadata\MetadataFactoryInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
|
||||
/**
|
||||
* Uses the JMS metadata factory to extract input/output model information.
|
||||
*/
|
||||
class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
|
||||
private $factory;
|
||||
|
||||
private $contextFactory;
|
||||
|
||||
private $namingStrategy;
|
||||
|
||||
/**
|
||||
* @var Reader|null
|
||||
*/
|
||||
private $doctrineReader;
|
||||
|
||||
private $contexts = [];
|
||||
|
||||
private $metadataStacks = [];
|
||||
|
||||
private $mediaTypes;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $propertyTypeUseGroupsCache = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $useValidationGroups;
|
||||
|
||||
public function __construct(
|
||||
MetadataFactoryInterface $factory,
|
||||
?Reader $reader,
|
||||
array $mediaTypes,
|
||||
?PropertyNamingStrategyInterface $namingStrategy = null,
|
||||
bool $useValidationGroups = false,
|
||||
?SerializationContextFactoryInterface $contextFactory = null
|
||||
) {
|
||||
$this->factory = $factory;
|
||||
$this->namingStrategy = $namingStrategy;
|
||||
$this->doctrineReader = $reader;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
$this->useValidationGroups = $useValidationGroups;
|
||||
$this->contextFactory = $contextFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
$className = $model->getType()->getClassName();
|
||||
$metadata = $this->factory->getMetadataForClass($className);
|
||||
if (null === $metadata) {
|
||||
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
|
||||
}
|
||||
|
||||
$annotationsReader = new AnnotationsReader(
|
||||
$this->doctrineReader,
|
||||
$this->modelRegistry,
|
||||
$this->mediaTypes,
|
||||
$this->useValidationGroups
|
||||
);
|
||||
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
|
||||
|
||||
if (!$classResult->shouldDescribeModelProperties()) {
|
||||
return;
|
||||
}
|
||||
$schema->type = 'object';
|
||||
|
||||
$isJmsV1 = null !== $this->namingStrategy;
|
||||
|
||||
$context = $this->getSerializationContext($model);
|
||||
$context->pushClassMetadata($metadata);
|
||||
foreach ($metadata->propertyMetadata as $item) {
|
||||
// filter groups
|
||||
if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->pushPropertyMetadata($item);
|
||||
|
||||
$name = true === $isJmsV1 ? $this->namingStrategy->translateName($item) : $item->serializedName;
|
||||
// read property options from Swagger Property annotation if it exists
|
||||
|
||||
$reflections = [];
|
||||
if (true === $isJmsV1 && property_exists($item, 'reflection') && null !== $item->reflection) {
|
||||
$reflections[] = $item->reflection;
|
||||
} elseif (\property_exists($item->class, $item->name)) {
|
||||
$reflections[] = new \ReflectionProperty($item->class, $item->name);
|
||||
}
|
||||
|
||||
if (null !== $item->getter) {
|
||||
try {
|
||||
$reflections[] = new \ReflectionMethod($item->class, $item->getter);
|
||||
} catch (\ReflectionException $ignored) {
|
||||
}
|
||||
}
|
||||
if (null !== $item->setter) {
|
||||
try {
|
||||
$reflections[] = new \ReflectionMethod($item->class, $item->setter);
|
||||
} catch (\ReflectionException $ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
$groups = $this->computeGroups($context, $item->type);
|
||||
|
||||
if (true === $item->inline && isset($item->type['name'])) {
|
||||
// currently array types can not be documented :-/
|
||||
if (!in_array($item->type['name'], ['array', 'ArrayCollection'], true)) {
|
||||
$inlineModel = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $item->type['name']), $groups);
|
||||
$this->describe($inlineModel, $schema);
|
||||
}
|
||||
$context->popPropertyMetadata();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($reflections as $reflection) {
|
||||
$name = $annotationsReader->getPropertyName($reflection, $name);
|
||||
}
|
||||
|
||||
$property = Util::getProperty($schema, $name);
|
||||
|
||||
foreach ($reflections as $reflection) {
|
||||
$annotationsReader->updateProperty($reflection, $property, $groups);
|
||||
}
|
||||
|
||||
if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
|
||||
$context->popPropertyMetadata();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Generator::UNDEFINED === $property->default && $item->hasDefault) {
|
||||
$property->default = $item->defaultValue;
|
||||
}
|
||||
|
||||
if (null === $item->type) {
|
||||
$key = Util::searchIndexedCollectionItem($schema->properties, 'property', $name);
|
||||
unset($schema->properties[$key]);
|
||||
$context->popPropertyMetadata();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->describeItem($item->type, $property, $context);
|
||||
$context->popPropertyMetadata();
|
||||
}
|
||||
$context->popClassMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getSerializationContext(Model $model): SerializationContext
|
||||
{
|
||||
if (isset($this->contexts[$model->getHash()])) {
|
||||
$context = $this->contexts[$model->getHash()];
|
||||
|
||||
$stack = $context->getMetadataStack();
|
||||
while (!$stack->isEmpty()) {
|
||||
$stack->pop();
|
||||
}
|
||||
|
||||
foreach ($this->metadataStacks[$model->getHash()] as $metadataCopy) {
|
||||
$stack->unshift($metadataCopy);
|
||||
}
|
||||
} else {
|
||||
$context = $this->contextFactory ? $this->contextFactory->createSerializationContext() : SerializationContext::create();
|
||||
|
||||
if (null !== $model->getGroups()) {
|
||||
$context->addExclusionStrategy(new GroupsExclusionStrategy($model->getGroups()));
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
private function computeGroups(Context $context, array $type = null)
|
||||
{
|
||||
if (null === $type || true !== $this->propertyTypeUsesGroups($type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$groupsExclusion = $context->getExclusionStrategy();
|
||||
if (!($groupsExclusion instanceof GroupsExclusionStrategy)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$groups = $groupsExclusion->getGroupsFor($context);
|
||||
if ([GroupsExclusionStrategy::DEFAULT_GROUP] === $groups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
$className = $model->getType()->getClassName();
|
||||
|
||||
try {
|
||||
if ($this->factory->getMetadataForClass($className)) {
|
||||
return true;
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function describeItem(array $type, OA\Schema $property, Context $context)
|
||||
{
|
||||
$nestedTypeInfo = $this->getNestedTypeInArray($type);
|
||||
if (null !== $nestedTypeInfo) {
|
||||
[$nestedType, $isHash] = $nestedTypeInfo;
|
||||
if ($isHash) {
|
||||
$property->type = 'object';
|
||||
$property->additionalProperties = Util::createChild($property, OA\Property::class);
|
||||
|
||||
// this is a free form object (as nested array)
|
||||
if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) {
|
||||
// in the case of a virtual property, set it as free object type
|
||||
$property->additionalProperties = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->describeItem($nestedType, $property->additionalProperties, $context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$property->type = 'array';
|
||||
$property->items = Util::createChild($property, OA\Items::class);
|
||||
$this->describeItem($nestedType, $property->items, $context);
|
||||
} elseif ('array' === $type['name']) {
|
||||
$property->type = 'object';
|
||||
$property->additionalProperties = true;
|
||||
} elseif ('string' === $type['name']) {
|
||||
$property->type = 'string';
|
||||
} elseif (in_array($type['name'], ['bool', 'boolean'], true)) {
|
||||
$property->type = 'boolean';
|
||||
} elseif (in_array($type['name'], ['int', 'integer'], true)) {
|
||||
$property->type = 'integer';
|
||||
} elseif (in_array($type['name'], ['double', 'float'], true)) {
|
||||
$property->type = 'number';
|
||||
$property->format = $type['name'];
|
||||
} elseif (is_a($type['name'], \DateTimeInterface::class, true)) {
|
||||
$property->type = 'string';
|
||||
$property->format = 'date-time';
|
||||
} else {
|
||||
// See https://github.com/schmittjoh/serializer/blob/5a5a03a/src/Metadata/Driver/EnumPropertiesDriver.php#L51
|
||||
if ('enum' === $type['name']
|
||||
&& isset($type['params'][0])
|
||||
&& function_exists('enum_exists')
|
||||
&& enum_exists($type['params'][0])
|
||||
) {
|
||||
$type = ['name' => $type['params'][0]];
|
||||
}
|
||||
|
||||
$groups = $this->computeGroups($context, $type);
|
||||
|
||||
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups);
|
||||
$modelRef = $this->modelRegistry->register($model);
|
||||
|
||||
$customFields = (array) $property->jsonSerialize();
|
||||
unset($customFields['property']);
|
||||
if (empty($customFields)) { // no custom fields
|
||||
$property->ref = $modelRef;
|
||||
} else {
|
||||
$weakContext = Util::createWeakContext($property->_context);
|
||||
$property->oneOf = [new OA\Schema(['ref' => $modelRef, '_context' => $weakContext])];
|
||||
}
|
||||
|
||||
$this->contexts[$model->getHash()] = $context;
|
||||
$this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack();
|
||||
}
|
||||
}
|
||||
|
||||
private function getNestedTypeInArray(array $type)
|
||||
{
|
||||
if ('array' !== $type['name'] && 'ArrayCollection' !== $type['name']) {
|
||||
return null;
|
||||
}
|
||||
// array<string, MyNamespaceMyObject>
|
||||
if (isset($type['params'][1]['name'])) {
|
||||
return [$type['params'][1], true];
|
||||
}
|
||||
// array<MyNamespaceMyObject>
|
||||
if (isset($type['params'][0]['name'])) {
|
||||
return [$type['params'][0], false];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|null
|
||||
*/
|
||||
private function propertyTypeUsesGroups(array $type)
|
||||
{
|
||||
if (array_key_exists($type['name'], $this->propertyTypeUseGroupsCache)) {
|
||||
return $this->propertyTypeUseGroupsCache[$type['name']];
|
||||
}
|
||||
|
||||
try {
|
||||
$metadata = $this->factory->getMetadataForClass($type['name']);
|
||||
|
||||
foreach ($metadata->propertyMetadata as $item) {
|
||||
if (null !== $item->groups && $item->groups != [GroupsExclusionStrategy::DEFAULT_GROUP]) {
|
||||
$this->propertyTypeUseGroupsCache[$type['name']] = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$this->propertyTypeUseGroupsCache[$type['name']] = false;
|
||||
|
||||
return false;
|
||||
} catch (\ReflectionException $e) {
|
||||
$this->propertyTypeUseGroupsCache[$type['name']] = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations\Schema;
|
||||
|
||||
interface ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, Schema $schema);
|
||||
|
||||
public function supports(Model $model): bool;
|
||||
}
|
||||
@@ -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\ModelDescriber;
|
||||
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
||||
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
|
||||
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
||||
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Generator;
|
||||
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
|
||||
use Symfony\Component\PropertyInfo\Type;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
|
||||
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
|
||||
|
||||
class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
|
||||
{
|
||||
use ModelRegistryAwareTrait;
|
||||
use ApplyOpenApiDiscriminatorTrait;
|
||||
|
||||
/** @var PropertyInfoExtractorInterface */
|
||||
private $propertyInfo;
|
||||
/** @var ClassMetadataFactoryInterface|null */
|
||||
private $classMetadataFactory;
|
||||
/** @var Reader|null */
|
||||
private $doctrineReader;
|
||||
/** @var PropertyDescriberInterface|PropertyDescriberInterface[] */
|
||||
private $propertyDescriber;
|
||||
/** @var string[] */
|
||||
private $mediaTypes;
|
||||
/** @var NameConverterInterface|null */
|
||||
private $nameConverter;
|
||||
/** @var bool */
|
||||
private $useValidationGroups;
|
||||
|
||||
/**
|
||||
* @param PropertyDescriberInterface|PropertyDescriberInterface[] $propertyDescribers
|
||||
*/
|
||||
public function __construct(
|
||||
PropertyInfoExtractorInterface $propertyInfo,
|
||||
?Reader $reader,
|
||||
$propertyDescribers,
|
||||
array $mediaTypes,
|
||||
NameConverterInterface $nameConverter = null,
|
||||
bool $useValidationGroups = false,
|
||||
ClassMetadataFactoryInterface $classMetadataFactory = null
|
||||
) {
|
||||
if (is_array($propertyDescribers)) {
|
||||
trigger_deprecation('nelmio/api-doc-bundle', '4.17', 'Passing an array of PropertyDescriberInterface to %s() is deprecated. Pass a single PropertyDescriberInterface instead.', __METHOD__);
|
||||
} else {
|
||||
if (!$propertyDescribers instanceof PropertyDescriberInterface) {
|
||||
throw new \InvalidArgumentException(sprintf('Argument 3 passed to %s() must be an array of %s or a single %s.', __METHOD__, PropertyDescriberInterface::class, PropertyDescriberInterface::class));
|
||||
}
|
||||
}
|
||||
|
||||
$this->propertyInfo = $propertyInfo;
|
||||
$this->doctrineReader = $reader;
|
||||
$this->propertyDescriber = $propertyDescribers;
|
||||
$this->mediaTypes = $mediaTypes;
|
||||
$this->nameConverter = $nameConverter;
|
||||
$this->useValidationGroups = $useValidationGroups;
|
||||
$this->classMetadataFactory = $classMetadataFactory;
|
||||
}
|
||||
|
||||
public function describe(Model $model, OA\Schema $schema)
|
||||
{
|
||||
$class = $model->getType()->getClassName();
|
||||
$schema->_context->class = $class;
|
||||
|
||||
$context = ['serializer_groups' => null];
|
||||
if (null !== $model->getGroups()) {
|
||||
$context['serializer_groups'] = array_filter($model->getGroups(), 'is_string');
|
||||
}
|
||||
|
||||
$reflClass = new \ReflectionClass($class);
|
||||
$annotationsReader = new AnnotationsReader(
|
||||
$this->doctrineReader,
|
||||
$this->modelRegistry,
|
||||
$this->mediaTypes,
|
||||
$this->useValidationGroups
|
||||
);
|
||||
$classResult = $annotationsReader->updateDefinition($reflClass, $schema);
|
||||
|
||||
if (!$classResult->shouldDescribeModelProperties()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema->type = 'object';
|
||||
|
||||
$mapping = false;
|
||||
if (null !== $this->classMetadataFactory) {
|
||||
$mapping = $this->classMetadataFactory
|
||||
->getMetadataFor($class)
|
||||
->getClassDiscriminatorMapping();
|
||||
}
|
||||
|
||||
if ($mapping && Generator::UNDEFINED === $schema->discriminator) {
|
||||
$this->applyOpenApiDiscriminator(
|
||||
$model,
|
||||
$schema,
|
||||
$this->modelRegistry,
|
||||
$mapping->getTypeProperty(),
|
||||
$mapping->getTypesMapping()
|
||||
);
|
||||
}
|
||||
|
||||
$propertyInfoProperties = $this->propertyInfo->getProperties($class, $context);
|
||||
|
||||
if (null === $propertyInfoProperties) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix for https://github.com/nelmio/NelmioApiDocBundle/issues/1756
|
||||
// The SerializerExtractor does expose private/protected properties for some reason, so we eliminate them here
|
||||
$propertyInfoProperties = array_intersect($propertyInfoProperties, $this->propertyInfo->getProperties($class, []) ?? []);
|
||||
|
||||
$defaultValues = array_filter($reflClass->getDefaultProperties(), static function ($value) {
|
||||
return null !== $value;
|
||||
});
|
||||
|
||||
foreach ($propertyInfoProperties as $propertyName) {
|
||||
$serializedName = null !== $this->nameConverter ? $this->nameConverter->normalize($propertyName, $class, null, $model->getSerializationContext()) : $propertyName;
|
||||
|
||||
$reflections = $this->getReflections($reflClass, $propertyName);
|
||||
|
||||
// Check if a custom name is set
|
||||
foreach ($reflections as $reflection) {
|
||||
$serializedName = $annotationsReader->getPropertyName($reflection, $serializedName);
|
||||
}
|
||||
|
||||
$property = Util::getProperty($schema, $serializedName);
|
||||
|
||||
// Interpret additional options
|
||||
$groups = $model->getGroups();
|
||||
if (isset($groups[$propertyName]) && is_array($groups[$propertyName])) {
|
||||
$groups = $model->getGroups()[$propertyName];
|
||||
}
|
||||
foreach ($reflections as $reflection) {
|
||||
$annotationsReader->updateProperty($reflection, $property, $groups);
|
||||
}
|
||||
|
||||
// If type manually defined
|
||||
if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Generator::UNDEFINED === $property->default && array_key_exists($propertyName, $defaultValues)) {
|
||||
$property->default = $defaultValues[$propertyName];
|
||||
}
|
||||
|
||||
$types = $this->propertyInfo->getTypes($class, $propertyName);
|
||||
if (null === $types || 0 === count($types)) {
|
||||
throw new \LogicException(sprintf('The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@OA\Property(type="")` to make its type explicit.', $class, $propertyName));
|
||||
}
|
||||
|
||||
$this->describeProperty($types, $model, $property, $propertyName, $schema);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \ReflectionProperty[]|\ReflectionMethod[]
|
||||
*/
|
||||
private function getReflections(\ReflectionClass $reflClass, string $propertyName): array
|
||||
{
|
||||
$reflections = [];
|
||||
if ($reflClass->hasProperty($propertyName)) {
|
||||
$reflections[] = $reflClass->getProperty($propertyName);
|
||||
}
|
||||
|
||||
$camelProp = $this->camelize($propertyName);
|
||||
foreach (['', 'get', 'is', 'has', 'can', 'add', 'remove', 'set'] as $prefix) {
|
||||
if ($reflClass->hasMethod($prefix.$camelProp)) {
|
||||
$reflections[] = $reflClass->getMethod($prefix.$camelProp);
|
||||
}
|
||||
}
|
||||
|
||||
return $reflections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Camelizes a given string.
|
||||
*/
|
||||
private function camelize(string $string): string
|
||||
{
|
||||
return str_replace(' ', '', ucwords(str_replace('_', ' ', $string)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type[] $types
|
||||
*/
|
||||
private function describeProperty(array $types, Model $model, OA\Schema $property, string $propertyName, OA\Schema $schema)
|
||||
{
|
||||
$propertyDescribers = is_array($this->propertyDescriber) ? $this->propertyDescriber : [$this->propertyDescriber];
|
||||
|
||||
foreach ($propertyDescribers as $propertyDescriber) {
|
||||
if ($propertyDescriber instanceof ModelRegistryAwareInterface) {
|
||||
$propertyDescriber->setModelRegistry($this->modelRegistry);
|
||||
}
|
||||
if ($propertyDescriber->supports($types)) {
|
||||
$propertyDescriber->describe($types, $property, $model->getGroups(), $schema, $model->getSerializationContext());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return Type::BUILTIN_TYPE_OBJECT === $model->getType()->getBuiltinType()
|
||||
&& (class_exists($model->getType()->getClassName()) || interface_exists($model->getType()->getClassName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations as OA;
|
||||
|
||||
class SelfDescribingModelDescriber implements ModelDescriberInterface
|
||||
{
|
||||
public function describe(Model $model, OA\Schema $schema): void
|
||||
{
|
||||
call_user_func([$model->getType()->getClassName(), 'describe'], $schema, $model);
|
||||
}
|
||||
|
||||
public function supports(Model $model): bool
|
||||
{
|
||||
return $model->getType()->getClassName()
|
||||
&& class_exists($model->getType()->getClassName())
|
||||
&& is_a($model->getType()->getClassName(), SelfDescribingModelInterface::class, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\ModelDescriber;
|
||||
|
||||
use Nelmio\ApiDocBundle\Model\Model;
|
||||
use OpenApi\Annotations\Schema;
|
||||
|
||||
/**
|
||||
* A self-describing model is a model able to describe its own schema through a static method call.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
interface SelfDescribingModelInterface
|
||||
{
|
||||
public static function describe(Schema $schema, Model $model): void;
|
||||
}
|
||||
Reference in New Issue
Block a user