welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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;
}