vendor/nelmio/api-doc-bundle/ModelDescriber/JMSModelDescriber.php line 205

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the NelmioApiDocBundle package.
  4.  *
  5.  * (c) Nelmio
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Nelmio\ApiDocBundle\ModelDescriber;
  11. use Doctrine\Common\Annotations\Reader;
  12. use JMS\Serializer\Context;
  13. use JMS\Serializer\ContextFactory\SerializationContextFactoryInterface;
  14. use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
  15. use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
  16. use JMS\Serializer\SerializationContext;
  17. use Metadata\MetadataFactoryInterface;
  18. use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
  19. use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
  20. use Nelmio\ApiDocBundle\Model\Model;
  21. use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
  22. use Nelmio\ApiDocBundle\OpenApiPhp\Util;
  23. use OpenApi\Annotations as OA;
  24. use OpenApi\Generator;
  25. use Symfony\Component\PropertyInfo\Type;
  26. /**
  27.  * Uses the JMS metadata factory to extract input/output model information.
  28.  */
  29. class JMSModelDescriber implements ModelDescriberInterfaceModelRegistryAwareInterface
  30. {
  31.     use ModelRegistryAwareTrait;
  32.     private $factory;
  33.     private $contextFactory;
  34.     private $namingStrategy;
  35.     private $doctrineReader;
  36.     private $contexts = [];
  37.     private $metadataStacks = [];
  38.     private $mediaTypes;
  39.     /**
  40.      * @var array
  41.      */
  42.     private $propertyTypeUseGroupsCache = [];
  43.     /**
  44.      * @var bool
  45.      */
  46.     private $useValidationGroups;
  47.     public function __construct(
  48.         MetadataFactoryInterface $factory,
  49.         Reader $reader,
  50.         array $mediaTypes,
  51.         ?PropertyNamingStrategyInterface $namingStrategy null,
  52.         bool $useValidationGroups false,
  53.         ?SerializationContextFactoryInterface $contextFactory null
  54.     ) {
  55.         $this->factory $factory;
  56.         $this->namingStrategy $namingStrategy;
  57.         $this->doctrineReader $reader;
  58.         $this->mediaTypes $mediaTypes;
  59.         $this->useValidationGroups $useValidationGroups;
  60.         $this->contextFactory $contextFactory;
  61.     }
  62.     /**
  63.      * {@inheritdoc}
  64.      */
  65.     public function describe(Model $modelOA\Schema $schema)
  66.     {
  67.         $className $model->getType()->getClassName();
  68.         $metadata $this->factory->getMetadataForClass($className);
  69.         if (null === $metadata) {
  70.             throw new \InvalidArgumentException(sprintf('No metadata found for class %s.'$className));
  71.         }
  72.         $annotationsReader = new AnnotationsReader(
  73.             $this->doctrineReader,
  74.             $this->modelRegistry,
  75.             $this->mediaTypes,
  76.             $this->useValidationGroups
  77.         );
  78.         $classResult $annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
  79.         if (!$classResult->shouldDescribeModelProperties()) {
  80.             return;
  81.         }
  82.         $schema->type 'object';
  83.         $isJmsV1 null !== $this->namingStrategy;
  84.         $context $this->getSerializationContext($model);
  85.         $context->pushClassMetadata($metadata);
  86.         foreach ($metadata->propertyMetadata as $item) {
  87.             // filter groups
  88.             if (null !== $context->getExclusionStrategy() && $context->getExclusionStrategy()->shouldSkipProperty($item$context)) {
  89.                 continue;
  90.             }
  91.             $context->pushPropertyMetadata($item);
  92.             $name true === $isJmsV1 $this->namingStrategy->translateName($item) : $item->serializedName;
  93.             // read property options from Swagger Property annotation if it exists
  94.             $reflections = [];
  95.             if (true === $isJmsV1 && property_exists($item'reflection') && null !== $item->reflection) {
  96.                 $reflections[] = $item->reflection;
  97.             } elseif (\property_exists($item->class$item->name)) {
  98.                 $reflections[] = new \ReflectionProperty($item->class$item->name);
  99.             }
  100.             if (null !== $item->getter) {
  101.                 try {
  102.                     $reflections[] = new \ReflectionMethod($item->class$item->getter);
  103.                 } catch (\ReflectionException $ignored) {
  104.                 }
  105.             }
  106.             if (null !== $item->setter) {
  107.                 try {
  108.                     $reflections[] = new \ReflectionMethod($item->class$item->setter);
  109.                 } catch (\ReflectionException $ignored) {
  110.                 }
  111.             }
  112.             $groups $this->computeGroups($context$item->type);
  113.             if (true === $item->inline && isset($item->type['name'])) {
  114.                 // currently array types can not be documented :-/
  115.                 if (!in_array($item->type['name'], ['array''ArrayCollection'], true)) {
  116.                     $inlineModel = new Model(new Type(Type::BUILTIN_TYPE_OBJECTfalse$item->type['name']), $groups);
  117.                     $this->describe($inlineModel$schema);
  118.                 }
  119.                 $context->popPropertyMetadata();
  120.                 continue;
  121.             }
  122.             foreach ($reflections as $reflection) {
  123.                 $name $annotationsReader->getPropertyName($reflection$name);
  124.             }
  125.             $property Util::getProperty($schema$name);
  126.             foreach ($reflections as $reflection) {
  127.                 $annotationsReader->updateProperty($reflection$property$groups);
  128.             }
  129.             if (Generator::UNDEFINED !== $property->type || Generator::UNDEFINED !== $property->ref) {
  130.                 $context->popPropertyMetadata();
  131.                 continue;
  132.             }
  133.             if (null === $item->type) {
  134.                 $key Util::searchIndexedCollectionItem($schema->properties'property'$name);
  135.                 unset($schema->properties[$key]);
  136.                 $context->popPropertyMetadata();
  137.                 continue;
  138.             }
  139.             $this->describeItem($item->type$property$context);
  140.             $context->popPropertyMetadata();
  141.         }
  142.         $context->popClassMetadata();
  143.     }
  144.     /**
  145.      * @internal
  146.      */
  147.     public function getSerializationContext(Model $model): SerializationContext
  148.     {
  149.         if (isset($this->contexts[$model->getHash()])) {
  150.             $context $this->contexts[$model->getHash()];
  151.             $stack $context->getMetadataStack();
  152.             while (!$stack->isEmpty()) {
  153.                 $stack->pop();
  154.             }
  155.             foreach ($this->metadataStacks[$model->getHash()] as $metadataCopy) {
  156.                 $stack->unshift($metadataCopy);
  157.             }
  158.         } else {
  159.             $context $this->contextFactory $this->contextFactory->createSerializationContext() : SerializationContext::create();
  160.             if (null !== $model->getGroups()) {
  161.                 $context->addExclusionStrategy(new GroupsExclusionStrategy($model->getGroups()));
  162.             }
  163.         }
  164.         return $context;
  165.     }
  166.     private function computeGroups(Context $context, array $type null)
  167.     {
  168.         if (null === $type || true !== $this->propertyTypeUsesGroups($type)) {
  169.             return null;
  170.         }
  171.         $groupsExclusion $context->getExclusionStrategy();
  172.         if (!($groupsExclusion instanceof GroupsExclusionStrategy)) {
  173.             return null;
  174.         }
  175.         $groups $groupsExclusion->getGroupsFor($context);
  176.         if ([GroupsExclusionStrategy::DEFAULT_GROUP] === $groups) {
  177.             return null;
  178.         }
  179.         return $groups;
  180.     }
  181.     /**
  182.      * {@inheritdoc}
  183.      */
  184.     public function supports(Model $model): bool
  185.     {
  186.         $className $model->getType()->getClassName();
  187.         try {
  188.             if ($this->factory->getMetadataForClass($className)) {
  189.                 return true;
  190.             }
  191.         } catch (\ReflectionException $e) {
  192.         }
  193.         return false;
  194.     }
  195.     /**
  196.      * @internal
  197.      */
  198.     public function describeItem(array $typeOA\Schema $propertyContext $context)
  199.     {
  200.         $nestedTypeInfo $this->getNestedTypeInArray($type);
  201.         if (null !== $nestedTypeInfo) {
  202.             list($nestedType$isHash) = $nestedTypeInfo;
  203.             if ($isHash) {
  204.                 $property->type 'object';
  205.                 $property->additionalProperties Util::createChild($propertyOA\Property::class);
  206.                 // this is a free form object (as nested array)
  207.                 if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) {
  208.                     // in the case of a virtual property, set it as free object type
  209.                     $property->additionalProperties true;
  210.                     return;
  211.                 }
  212.                 $this->describeItem($nestedType$property->additionalProperties$context);
  213.                 return;
  214.             }
  215.             $property->type 'array';
  216.             $property->items Util::createChild($propertyOA\Items::class);
  217.             $this->describeItem($nestedType$property->items$context);
  218.         } elseif ('array' === $type['name']) {
  219.             $property->type 'object';
  220.             $property->additionalProperties true;
  221.         } elseif ('string' === $type['name']) {
  222.             $property->type 'string';
  223.         } elseif (in_array($type['name'], ['bool''boolean'], true)) {
  224.             $property->type 'boolean';
  225.         } elseif (in_array($type['name'], ['int''integer'], true)) {
  226.             $property->type 'integer';
  227.         } elseif (in_array($type['name'], ['double''float'], true)) {
  228.             $property->type 'number';
  229.             $property->format $type['name'];
  230.         } elseif (is_a($type['name'], \DateTimeInterface::class, true)) {
  231.             $property->type 'string';
  232.             $property->format 'date-time';
  233.         } else {
  234.             $groups $this->computeGroups($context$type);
  235.             $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECTfalse$type['name']), $groups);
  236.             $modelRef $this->modelRegistry->register($model);
  237.             $customFields = (array) $property->jsonSerialize();
  238.             unset($customFields['property']);
  239.             if (empty($customFields)) { // no custom fields
  240.                 $property->ref $modelRef;
  241.             } else {
  242.                 $weakContext Util::createWeakContext($property->_context);
  243.                 $property->allOf = [new OA\Schema(['ref' => $modelRef'_context' => $weakContext])];
  244.             }
  245.             $this->contexts[$model->getHash()] = $context;
  246.             $this->metadataStacks[$model->getHash()] = clone $context->getMetadataStack();
  247.         }
  248.     }
  249.     private function getNestedTypeInArray(array $type)
  250.     {
  251.         if ('array' !== $type['name'] && 'ArrayCollection' !== $type['name']) {
  252.             return null;
  253.         }
  254.         // array<string, MyNamespaceMyObject>
  255.         if (isset($type['params'][1]['name'])) {
  256.             return [$type['params'][1], true];
  257.         }
  258.         // array<MyNamespaceMyObject>
  259.         if (isset($type['params'][0]['name'])) {
  260.             return [$type['params'][0], false];
  261.         }
  262.         return null;
  263.     }
  264.     /**
  265.      * @return bool|null
  266.      */
  267.     private function propertyTypeUsesGroups(array $type)
  268.     {
  269.         if (array_key_exists($type['name'], $this->propertyTypeUseGroupsCache)) {
  270.             return $this->propertyTypeUseGroupsCache[$type['name']];
  271.         }
  272.         try {
  273.             $metadata $this->factory->getMetadataForClass($type['name']);
  274.             foreach ($metadata->propertyMetadata as $item) {
  275.                 if (null !== $item->groups && $item->groups != [GroupsExclusionStrategy::DEFAULT_GROUP]) {
  276.                     $this->propertyTypeUseGroupsCache[$type['name']] = true;
  277.                     return true;
  278.                 }
  279.             }
  280.             $this->propertyTypeUseGroupsCache[$type['name']] = false;
  281.             return false;
  282.         } catch (\ReflectionException $e) {
  283.             $this->propertyTypeUseGroupsCache[$type['name']] = null;
  284.             return null;
  285.         }
  286.     }
  287. }