NelmioApiDocBundle/ModelDescriber/JMSModelDescriber.php

283 lines
9.2 KiB
PHP
Raw Normal View History

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;
use Doctrine\Common\Annotations\Reader;
2017-06-25 15:40:07 +02:00
use EXSyst\Component\Swagger\Schema;
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;
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader;
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
private $doctrineReader;
2019-03-11 12:53:35 +01:00
private $contexts = [];
private $metadataStacks = [];
/**
* @var array
*/
private $propertyTypeUseGroupsCache = [];
2017-12-19 08:41:24 +01:00
public function __construct(
MetadataFactoryInterface $factory,
PropertyNamingStrategyInterface $namingStrategy = null,
Reader $reader
2017-12-17 10:44:07 +01:00
) {
2017-06-25 15:40:07 +02:00
$this->factory = $factory;
$this->namingStrategy = $namingStrategy;
$this->doctrineReader = $reader;
2017-06-25 15:40:07 +02:00
}
/**
* {@inheritdoc}
*/
public function describe(Model $model, 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));
}
$schema->setType('object');
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
$annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
2019-01-26 14:52:13 +01:00
$isJmsV1 = null !== $this->namingStrategy;
2017-06-25 15:40:07 +02:00
$properties = $schema->getProperties();
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);
2019-01-26 14:52:13 +01:00
$name = true === $isJmsV1 ? $this->namingStrategy->translateName($item) : $item->serializedName;
// read property options from Swagger Property annotation if it exists
2019-01-11 10:40:53 +01:00
try {
2019-01-26 14:52:13 +01:00
if (true === $isJmsV1 && property_exists($item, 'reflection') && null !== $item->reflection) {
2019-01-11 10:40:53 +01:00
$reflection = $item->reflection;
} else {
$reflection = new \ReflectionProperty($item->class, $item->name);
}
$property = $properties->get($annotationsReader->getPropertyName($reflection, $name));
2019-03-11 12:53:35 +01:00
$groups = $this->computeGroups($context, $item->type);
2019-01-11 10:40:53 +01:00
$annotationsReader->updateProperty($reflection, $property, $groups);
} catch (\ReflectionException $e) {
2018-02-19 21:41:05 +01:00
$property = $properties->get($name);
}
if (null !== $property->getType() || null !== $property->getRef()) {
2019-03-11 12:53:35 +01:00
$context->popPropertyMetadata();
continue;
}
if (null === $item->type) {
$properties->remove($name);
2019-03-11 12:53:35 +01:00
$context->popPropertyMetadata();
continue;
}
2017-06-25 15:40:07 +02:00
2019-03-11 12:53:35 +01:00
$this->describeItem($item->type, $property, $context, $item);
$context->popPropertyMetadata();
}
$context->popClassMetadata();
}
/**
* @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;
}
/**
* @internal
*/
public function describeItem(array $type, $property, Context $context)
2017-06-25 15:40:07 +02:00
{
$nestedTypeInfo = $this->getNestedTypeInArray($type);
if (null !== $nestedTypeInfo) {
list($nestedType, $isHash) = $nestedTypeInfo;
if ($isHash) {
$property->setType('object');
// in the case of a virtual property, set it as free object type
$property->merge(['additionalProperties' => []]);
// this is a free form object (as nested array)
if ('array' === $nestedType['name'] && !isset($nestedType['params'][0])) {
return;
}
2019-03-11 12:53:35 +01:00
$this->describeItem($nestedType, $property->getAdditionalProperties(), $context);
return;
}
$property->setType('array');
2019-03-11 12:53:35 +01:00
$this->describeItem($nestedType, $property->getItems(), $context);
} elseif ('array' === $type['name']) {
$property->setType('object');
$property->merge(['additionalProperties' => []]);
} elseif ('string' === $type['name']) {
$property->setType('string');
} elseif (in_array($type['name'], ['bool', 'boolean'], true)) {
$property->setType('boolean');
} elseif (in_array($type['name'], ['int', 'integer'], true)) {
$property->setType('integer');
} elseif (in_array($type['name'], ['double', 'float'], true)) {
$property->setType('number');
$property->setFormat($type['name']);
} elseif (is_subclass_of($type['name'], \DateTimeInterface::class)) {
$property->setType('string');
$property->setFormat('date-time');
} else {
2019-03-11 12:53:35 +01:00
$groups = $this->computeGroups($context, $type);
$model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups);
$property->setRef($this->modelRegistry->register($model));
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
}
}
2017-06-25 15:40:07 +02: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>
if (isset($type['params'][0]['name'])) {
return [$type['params'][0], false];
2017-06-25 15:40:07 +02:00
}
return null;
2017-06-25 15:40:07 +02:00
}
/**
* @param array $type
*
* @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;
}
}
2017-06-25 15:40:07 +02:00
}