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;
|
2017-06-25 15:40:07 +02:00
|
|
|
use EXSyst\Component\Swagger\Schema;
|
|
|
|
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;
|
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;
|
|
|
|
private $namingStrategy;
|
2018-03-17 19:23:29 +01:00
|
|
|
private $doctrineReader;
|
2017-12-19 08:41:24 +01:00
|
|
|
|
2017-12-03 20:30:44 +02:00
|
|
|
public function __construct(
|
|
|
|
MetadataFactoryInterface $factory,
|
|
|
|
PropertyNamingStrategyInterface $namingStrategy,
|
2018-01-24 19:58:38 +01:00
|
|
|
Reader $reader
|
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;
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
$groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null;
|
|
|
|
|
|
|
|
$schema->setType('object');
|
2018-03-17 19:23:29 +01:00
|
|
|
$annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry);
|
|
|
|
$annotationsReader->updateDefinition(new \ReflectionClass($className), $schema);
|
2018-01-24 19:58:38 +01:00
|
|
|
|
2017-06-25 15:40:07 +02:00
|
|
|
$properties = $schema->getProperties();
|
|
|
|
foreach ($metadata->propertyMetadata as $item) {
|
|
|
|
// filter groups
|
|
|
|
if (null !== $groupsExclusion && $groupsExclusion->shouldSkipProperty($item, SerializationContext::create())) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$name = $this->namingStrategy->translateName($item);
|
2018-03-17 19:23:29 +01:00
|
|
|
$groups = $model->getGroups();
|
|
|
|
if (isset($groups[$name]) && is_array($groups[$name])) {
|
|
|
|
$groups = $model->getGroups()[$name];
|
2018-07-26 14:14:26 +02:00
|
|
|
} elseif (is_array($groups)) {
|
|
|
|
$groups = array_filter($groups, 'is_scalar');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([GroupsExclusionStrategy::DEFAULT_GROUP] === $groups) {
|
|
|
|
$groups = null;
|
2018-03-17 19:23:29 +01:00
|
|
|
}
|
2018-01-11 12:26:59 +01:00
|
|
|
|
|
|
|
// read property options from Swagger Property annotation if it exists
|
|
|
|
if (null !== $item->reflection) {
|
2018-03-17 19:23:29 +01:00
|
|
|
$property = $properties->get($annotationsReader->getPropertyName($item->reflection, $name));
|
|
|
|
$annotationsReader->updateProperty($item->reflection, $property, $groups);
|
2018-02-19 21:41:05 +01:00
|
|
|
} else {
|
|
|
|
$property = $properties->get($name);
|
2018-01-11 12:26:59 +01:00
|
|
|
}
|
|
|
|
|
2018-03-17 19:23:29 +01:00
|
|
|
if (null !== $property->getType() || null !== $property->getRef()) {
|
2018-01-11 12:26:59 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (null === $item->type) {
|
|
|
|
$properties->remove($name);
|
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
|
|
|
|
2018-07-23 13:35:05 -03:00
|
|
|
$this->describeItem($item->type, $property, $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;
|
|
|
|
}
|
|
|
|
|
2018-08-21 12:41:32 -03:00
|
|
|
private function describeItem(array $type, Schema $property, array $groups = null)
|
2017-06-25 15:40:07 +02:00
|
|
|
{
|
2018-07-23 13:35:05 -03:00
|
|
|
if (list($nestedType, $isHash) = $this->getNestedTypeInArray($type)) { // @ todo update a bit getNestedTypeInArray and describe ($type = $item->type)
|
|
|
|
if ($isHash) {
|
|
|
|
$property->setType('object');
|
|
|
|
// in the case of a virtual property, set it as free object type
|
|
|
|
$property->merge(['additionalProperties' => []]);
|
|
|
|
|
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])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-23 13:35:05 -03:00
|
|
|
$this->describeItem($nestedType, $property->getAdditionalProperties(), $groups);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$property->setType('array');
|
|
|
|
$this->describeItem($nestedType, $property->getItems(), $groups);
|
2018-08-21 12:41:32 -03:00
|
|
|
} elseif ('array' === $type['name']) {
|
|
|
|
$property->setType('object');
|
|
|
|
$property->merge(['additionalProperties' => []]);
|
|
|
|
} elseif (in_array($type['name'], ['boolean', 'string'], true)) {
|
|
|
|
$property->setType($type['name']);
|
|
|
|
} 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 {
|
|
|
|
// we can use property type also for custom handlers, then we don't have here real class name
|
|
|
|
if (!class_exists($type['name'])) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-06-25 15:40:07 +02:00
|
|
|
|
2018-08-21 12:41:32 -03:00
|
|
|
$property->setRef($this->modelRegistry->register(
|
|
|
|
new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type['name']), $groups)
|
|
|
|
));
|
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
|
|
|
}
|
|
|
|
}
|