2017-06-25 15:40:07 +02:00
|
|
|
<?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;
|
|
|
|
|
2018-01-24 19:58:38 +01:00
|
|
|
use Doctrine\Common\Annotations\Reader;
|
2019-03-11 12:53:35 +01:00
|
|
|
use JMS\Serializer\Context;
|
2017-06-25 15:40:07 +02:00
|
|
|
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;
|
2018-01-24 19:58:38 +01:00
|
|
|
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
|
2020-05-28 13:19:11 +02:00
|
|
|
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
|
|
|
use OpenApi\Annotations as OA;
|
2021-12-11 16:39:04 +03:00
|
|
|
use OpenApi\Generator;
|
2017-06-25 15:40:07 +02:00
|
|
|
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;
|
2019-03-11 12:53:35 +01:00
|
|
|
|
2017-06-25 15:40:07 +02:00
|
|
|
private $namingStrategy;
|
2019-03-11 12:53:35 +01:00
|
|
|
|
2018-03-17 19:23:29 +01:00
|
|
|
private $doctrineReader;
|
2019-03-11 12:53:35 +01:00
|
|
|
|
|
|
|
private $contexts = [];
|
|
|
|
|
|
|
|
private $metadataStacks = [];
|
2018-05-02 16:56:20 +02:00
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
private $mediaTypes;
|
|
|
|
|
2018-05-02 16:56:20 +02:00
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
private $propertyTypeUseGroupsCache = [];
|
2017-12-19 08:41:24 +01:00
|
|
|
|
2017-12-03 20:30:44 +02:00
|
|
|
public function __construct(
|
|
|
|
MetadataFactoryInterface $factory,
|
2020-05-28 13:19:11 +02:00
|
|
|
Reader $reader,
|
|
|
|
array $mediaTypes,
|
|
|
|
?PropertyNamingStrategyInterface $namingStrategy = null
|
2017-12-17 10:44:07 +01:00
|
|
|
) {
|
2017-06-25 15:40:07 +02:00
|
|
|
$this->factory = $factory;
|
|
|
|
$this->namingStrategy = $namingStrategy;
|
2018-03-17 19:23:29 +01:00
|
|
|
$this->doctrineReader = $reader;
|
2020-05-28 13:19:11 +02:00
|
|
|
$this->mediaTypes = $mediaTypes;
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
2020-05-28 13:19:11 +02:00
|
|
|
public function describe(Model $model, OA\Schema $schema)
|
2017-06-25 15:40:07 +02:00
|
|
|
{
|
|
|
|
$className = $model->getType()->getClassName();
|
|
|
|
$metadata = $this->factory->getMetadataForClass($className);
|
|
|
|
if (null === $metadata) {
|
|
|
|
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
|
2022-04-30 13:28:05 -05:00
|
|
|
$classResult = $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
|
|
|
|
|
|
|
|
if (!$classResult->shouldDescribeModelProperties()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$schema->type = 'object';
|
2018-01-24 19:58:38 +01:00
|
|
|
|
2019-01-26 14:52:13 +01:00
|
|
|
$isJmsV1 = null !== $this->namingStrategy;
|
2019-03-11 12:53:35 +01:00
|
|
|
|
|
|
|
$context = $this->getSerializationContext($model);
|
|
|
|
$context->pushClassMetadata($metadata);
|
2017-06-25 15:40:07 +02:00
|
|
|
foreach ($metadata->propertyMetadata as $item) {
|
|
|
|
// filter groups
|
2019-03-11 12:53:35 +01:00
|
|
|
if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item, $context)) {
|
2017-06-25 15:40:07 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-03-11 12:53:35 +01:00
|
|
|
$context->pushPropertyMetadata($item);
|
2018-01-11 12:26:59 +01:00
|
|
|
|
2019-01-26 14:52:13 +01:00
|
|
|
$name = true === $isJmsV1 ? $this->namingStrategy->translateName($item) : $item->serializedName;
|
2018-01-11 12:26:59 +01:00
|
|
|
// read property options from Swagger Property annotation if it exists
|
2019-01-11 10:40:53 +01:00
|
|
|
|
2020-07-18 10:10:37 +02:00
|
|
|
$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);
|
|
|
|
}
|
|
|
|
|
2020-09-09 08:35:01 +02:00
|
|
|
if (null !== $item->getter) {
|
|
|
|
try {
|
2020-09-09 02:27:47 -04:00
|
|
|
$reflections[] = new \ReflectionMethod($item->class, $item->getter);
|
2020-09-09 08:35:01 +02:00
|
|
|
} catch (\ReflectionException $ignored) {
|
2020-09-09 02:27:47 -04:00
|
|
|
}
|
2020-09-09 08:35:01 +02:00
|
|
|
}
|
|
|
|
if (null !== $item->setter) {
|
|
|
|
try {
|
2020-09-09 02:27:47 -04:00
|
|
|
$reflections[] = new \ReflectionMethod($item->class, $item->setter);
|
2020-09-09 08:35:01 +02:00
|
|
|
} catch (\ReflectionException $ignored) {
|
2020-09-09 02:27:47 -04:00
|
|
|
}
|
2020-07-18 10:10:37 +02:00
|
|
|
}
|
2020-05-07 20:00:01 +02:00
|
|
|
|
2020-07-18 10:10:37 +02:00
|
|
|
$groups = $this->computeGroups($context, $item->type);
|
2020-05-07 20:00:01 +02:00
|
|
|
|
2020-07-18 10:10:37 +02:00
|
|
|
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);
|
2020-05-07 20:00:01 +02:00
|
|
|
}
|
2020-07-18 10:10:37 +02:00
|
|
|
$context->popPropertyMetadata();
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($reflections as $reflection) {
|
|
|
|
$name = $annotationsReader->getPropertyName($reflection, $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
$property = Util::getProperty($schema, $name);
|
2020-05-07 20:00:01 +02:00
|
|
|
|
2020-07-18 10:10:37 +02:00
|
|
|
foreach ($reflections as $reflection) {
|
2019-01-11 10:40:53 +01:00
|
|
|
$annotationsReader->updateProperty($reflection, $property, $groups);
|
2018-01-11 12:26:59 +01:00
|
|
|
}
|
|
|
|
|
2021-12-11 16:39:04 +03:00
|
|
|
if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
|
2019-03-11 12:53:35 +01:00
|
|
|
$context->popPropertyMetadata();
|
|
|
|
|
2018-01-11 12:26:59 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (null === $item->type) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$key = Util::searchIndexedCollectionItem($schema->properties, 'property', $name);
|
|
|
|
unset($schema->properties[$key]);
|
2019-03-11 12:53:35 +01:00
|
|
|
$context->popPropertyMetadata();
|
2018-03-17 19:23:29 +01:00
|
|
|
|
|
|
|
continue;
|
2018-01-11 12:26:59 +01:00
|
|
|
}
|
2017-06-25 15:40:07 +02:00
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$this->describeItem($item->type, $property, $context);
|
2019-03-11 12:53:35 +01:00
|
|
|
$context->popPropertyMetadata();
|
|
|
|
}
|
|
|
|
$context->popClassMetadata();
|
|
|
|
}
|
|
|
|
|
2019-05-02 10:02:16 +02:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public function getSerializationContext(Model $model): SerializationContext
|
2019-03-11 12:53:35 +01:00
|
|
|
{
|
|
|
|
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 = 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;
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|
2019-03-11 12:53:35 +01:00
|
|
|
|
|
|
|
return $groups;
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function supports(Model $model): bool
|
|
|
|
{
|
|
|
|
$className = $model->getType()->getClassName();
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2017-06-25 15:40:07 +02:00
|
|
|
try {
|
|
|
|
if ($this->factory->getMetadataForClass($className)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} catch (\ReflectionException $e) {
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-05-02 10:02:16 +02:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2020-05-28 13:19:11 +02:00
|
|
|
public function describeItem(array $type, OA\Schema $property, Context $context)
|
2017-06-25 15:40:07 +02:00
|
|
|
{
|
2018-05-02 16:56:20 +02:00
|
|
|
$nestedTypeInfo = $this->getNestedTypeInArray($type);
|
|
|
|
if (null !== $nestedTypeInfo) {
|
|
|
|
list($nestedType, $isHash) = $nestedTypeInfo;
|
2018-07-23 13:35:05 -03:00
|
|
|
if ($isHash) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'object';
|
|
|
|
$property->additionalProperties = Util::createChild($property, OA\Property::class);
|
2018-07-23 13:35:05 -03:00
|
|
|
|
2018-08-21 12:41:32 -03:00
|
|
|
// this is a free form object (as nested array)
|
|
|
|
if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) {
|
2020-05-28 13:19:11 +02:00
|
|
|
// in the case of a virtual property, set it as free object type
|
|
|
|
$property->additionalProperties = true;
|
|
|
|
|
2018-08-21 12:41:32 -03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$this->describeItem($nestedType, $property->additionalProperties, $context);
|
2018-07-23 13:35:05 -03:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'array';
|
|
|
|
$property->items = Util::createChild($property, OA\Items::class);
|
|
|
|
$this->describeItem($nestedType, $property->items, $context);
|
2018-08-21 12:41:32 -03:00
|
|
|
} elseif ('array' === $type['name']) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'object';
|
|
|
|
$property->additionalProperties = true;
|
2019-11-15 01:06:26 +05:00
|
|
|
} elseif ('string' === $type['name']) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
2019-11-15 01:06:26 +05:00
|
|
|
} elseif (in_array($type['name'], ['bool', 'boolean'], true)) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'boolean';
|
2018-08-21 12:41:32 -03:00
|
|
|
} elseif (in_array($type['name'], ['int', 'integer'], true)) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'integer';
|
2018-08-21 12:41:32 -03:00
|
|
|
} elseif (in_array($type['name'], ['double', 'float'], true)) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'number';
|
|
|
|
$property->format = $type['name'];
|
2018-08-21 12:41:32 -03:00
|
|
|
} elseif (is_subclass_of($type['name'], \DateTimeInterface::class)) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
|
|
|
$property->format = 'date-time';
|
2018-08-21 12:41:32 -03:00
|
|
|
} else {
|
2019-03-11 12:53:35 +01:00
|
|
|
$groups = $this->computeGroups($context, $type);
|
|
|
|
|
2018-10-10 17:24:13 +02:00
|
|
|
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups);
|
2020-09-27 22:20:04 +02:00
|
|
|
$modelRef = $this->modelRegistry->register($model);
|
|
|
|
|
|
|
|
$customFields = (array) $property->jsonSerialize();
|
|
|
|
unset($customFields['property']);
|
|
|
|
if (empty($customFields)) { // no custom fields
|
|
|
|
$property->ref = $modelRef;
|
|
|
|
} else {
|
|
|
|
$property->allOf = [new OA\Schema(['ref' => $modelRef])];
|
|
|
|
}
|
2018-05-02 16:56:20 +02:00
|
|
|
|
2019-03-11 12:53:35 +01:00
|
|
|
$this->contexts[$model->getHash()] = $context;
|
|
|
|
$this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack();
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|
2018-07-23 13:35:05 -03:00
|
|
|
}
|
2017-06-25 15:40:07 +02:00
|
|
|
|
2018-07-23 13:35:05 -03:00
|
|
|
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];
|
|
|
|
}
|
2017-06-25 15:40:07 +02:00
|
|
|
// array<MyNamespaceMyObject>
|
2018-07-23 13:35:05 -03:00
|
|
|
if (isset($type['params'][0]['name'])) {
|
|
|
|
return [$type['params'][0], false];
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|
2018-04-21 16:57:37 +02:00
|
|
|
|
|
|
|
return null;
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|
2018-05-02 16:56:20 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool|null
|
|
|
|
*/
|
|
|
|
private function propertyTypeUsesGroups(array $type)
|
|
|
|
{
|
2018-10-10 17:24:13 +02:00
|
|
|
if (array_key_exists($type['name'], $this->propertyTypeUseGroupsCache)) {
|
2018-05-02 16:56:20 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2017-06-25 15:40:07 +02:00
|
|
|
}
|