mirror of
https://github.com/retailcrm/NelmioApiDocBundle.git
synced 2025-02-02 15:51:48 +03:00
37ae52ba0a
Don't let empty arrays get passed through as defaults.
328 lines
11 KiB
PHP
328 lines
11 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the NelmioApiDocBundle.
|
|
*
|
|
* (c) Nelmio <hello@nelm.io>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Nelmio\ApiDocBundle\Parser;
|
|
|
|
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
|
|
use JMS\Serializer\SerializationContext;
|
|
use Metadata\MetadataFactoryInterface;
|
|
use Nelmio\ApiDocBundle\DataTypes;
|
|
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
|
|
use JMS\Serializer\Metadata\PropertyMetadata;
|
|
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
|
|
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
|
|
|
|
/**
|
|
* Uses the JMS metadata factory to extract input/output model information
|
|
*/
|
|
class JmsMetadataParser implements ParserInterface, PostParserInterface
|
|
{
|
|
/**
|
|
* @var \Metadata\MetadataFactoryInterface
|
|
*/
|
|
private $factory;
|
|
|
|
/**
|
|
* @var PropertyNamingStrategyInterface
|
|
*/
|
|
private $namingStrategy;
|
|
|
|
/**
|
|
* @var \Nelmio\ApiDocBundle\Util\DocCommentExtractor
|
|
*/
|
|
private $commentExtractor;
|
|
|
|
private $typeMap = array(
|
|
'integer' => DataTypes::INTEGER,
|
|
'boolean' => DataTypes::BOOLEAN,
|
|
'string' => DataTypes::STRING,
|
|
'float' => DataTypes::FLOAT,
|
|
'double' => DataTypes::FLOAT,
|
|
'array' => DataTypes::COLLECTION,
|
|
'DateTime' => DataTypes::DATETIME,
|
|
);
|
|
/**
|
|
* Constructor, requires JMS Metadata factory
|
|
*/
|
|
public function __construct(
|
|
MetadataFactoryInterface $factory,
|
|
PropertyNamingStrategyInterface $namingStrategy,
|
|
DocCommentExtractor $commentExtractor
|
|
) {
|
|
$this->factory = $factory;
|
|
$this->namingStrategy = $namingStrategy;
|
|
$this->commentExtractor = $commentExtractor;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function supports(array $input)
|
|
{
|
|
$className = $input['class'];
|
|
|
|
try {
|
|
if ($meta = $this->factory->getMetadataForClass($className)) {
|
|
return true;
|
|
}
|
|
} catch (\ReflectionException $e) {
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function parse(array $input)
|
|
{
|
|
$className = $input['class'];
|
|
$groups = $input['groups'];
|
|
|
|
return $this->doParse($className, array(), $groups);
|
|
}
|
|
|
|
/**
|
|
* Recursively parse all metadata for a class
|
|
*
|
|
* @param string $className Class to get all metadata for
|
|
* @param array $visited Classes we've already visited to prevent infinite recursion.
|
|
* @param array $groups Serialization groups to include.
|
|
* @return array metadata for given class
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
protected function doParse($className, $visited = array(), array $groups = array())
|
|
{
|
|
$meta = $this->factory->getMetadataForClass($className);
|
|
|
|
if (null === $meta) {
|
|
throw new \InvalidArgumentException(sprintf("No metadata found for class %s", $className));
|
|
}
|
|
|
|
$exclusionStrategies = array();
|
|
if ($groups) {
|
|
$exclusionStrategies[] = new GroupsExclusionStrategy($groups);
|
|
}
|
|
|
|
$params = array();
|
|
|
|
$reflection = new \ReflectionClass($className);
|
|
$defaultProperties = array_map(function ($default) {
|
|
if (is_array($default) && count($default) === 0) {
|
|
return null;
|
|
}
|
|
|
|
return $default;
|
|
}, $reflection->getDefaultProperties());
|
|
|
|
// iterate over property metadata
|
|
foreach ($meta->propertyMetadata as $item) {
|
|
if (!is_null($item->type)) {
|
|
$name = $this->namingStrategy->translateName($item);
|
|
|
|
$dataType = $this->processDataType($item);
|
|
|
|
// apply exclusion strategies
|
|
foreach ($exclusionStrategies as $strategy) {
|
|
if (true === $strategy->shouldSkipProperty($item, SerializationContext::create())) {
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
if (!$dataType['inline']) {
|
|
$params[$name] = array(
|
|
'dataType' => $dataType['normalized'],
|
|
'actualType' => $dataType['actualType'],
|
|
'subType' => $dataType['class'],
|
|
'required' => false,
|
|
'default' => isset($defaultProperties[$item->name]) ? $defaultProperties[$item->name] : null,
|
|
//TODO: can't think of a good way to specify this one, JMS doesn't have a setting for this
|
|
'description' => $this->getDescription($item),
|
|
'readonly' => $item->readOnly,
|
|
'sinceVersion' => $item->sinceVersion,
|
|
'untilVersion' => $item->untilVersion,
|
|
);
|
|
|
|
if (!is_null($dataType['class']) && false === $dataType['primitive']) {
|
|
$params[$name]['class'] = $dataType['class'];
|
|
}
|
|
}
|
|
|
|
// we can use type property also for custom handlers, then we don't have here real class name
|
|
if (!class_exists($dataType['class'])) {
|
|
continue;
|
|
}
|
|
|
|
// if class already parsed, continue, to avoid infinite recursion
|
|
if (in_array($dataType['class'], $visited)) {
|
|
continue;
|
|
}
|
|
|
|
// check for nested classes with JMS metadata
|
|
if ($dataType['class'] && false === $dataType['primitive'] && null !== $this->factory->getMetadataForClass($dataType['class'])) {
|
|
$visited[] = $dataType['class'];
|
|
$children = $this->doParse($dataType['class'], $visited, $groups);
|
|
|
|
if ($dataType['inline']) {
|
|
$params = array_merge($params, $children);
|
|
} else {
|
|
$params[$name]['children'] = $children;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $params;
|
|
}
|
|
|
|
/**
|
|
* Figure out a normalized data type (for documentation), and get a
|
|
* nested class name, if available.
|
|
*
|
|
* @param PropertyMetadata $item
|
|
* @return array
|
|
*/
|
|
protected function processDataType(PropertyMetadata $item)
|
|
{
|
|
// check for a type inside something that could be treated as an array
|
|
if ($nestedType = $this->getNestedTypeInArray($item)) {
|
|
if ($this->isPrimitive($nestedType)) {
|
|
return array(
|
|
'normalized' => sprintf("array of %ss", $nestedType),
|
|
'actualType' => DataTypes::COLLECTION,
|
|
'class' => $this->typeMap[$nestedType],
|
|
'primitive' => true,
|
|
'inline' => false,
|
|
);
|
|
}
|
|
|
|
$exp = explode("\\", $nestedType);
|
|
|
|
return array(
|
|
'normalized' => sprintf("array of objects (%s)", end($exp)),
|
|
'actualType' => DataTypes::COLLECTION,
|
|
'class' => $nestedType,
|
|
'primitive' => false,
|
|
'inline' => false,
|
|
);
|
|
}
|
|
|
|
$type = $item->type['name'];
|
|
|
|
// could be basic type
|
|
if ($this->isPrimitive($type)) {
|
|
return array(
|
|
'normalized' => $type,
|
|
'actualType' => $this->typeMap[$type],
|
|
'class' => null,
|
|
'primitive' => true,
|
|
'inline' => false,
|
|
);
|
|
}
|
|
|
|
// we can use type property also for custom handlers, then we don't have here real class name
|
|
if (!class_exists($type)) {
|
|
return array(
|
|
'normalized' => sprintf("custom handler result for (%s)", $type),
|
|
'class' => $type,
|
|
'actualType' => DataTypes::MODEL,
|
|
'primitive' => false,
|
|
'inline' => false,
|
|
);
|
|
}
|
|
|
|
// if we got this far, it's a general class name
|
|
$exp = explode("\\", $type);
|
|
|
|
return array(
|
|
'normalized' => sprintf("object (%s)", end($exp)),
|
|
'class' => $type,
|
|
'actualType' => DataTypes::MODEL,
|
|
'primitive' => false,
|
|
'inline' => $item->inline,
|
|
);
|
|
}
|
|
|
|
protected function isPrimitive($type)
|
|
{
|
|
return in_array($type, array('boolean', 'integer', 'string', 'float', 'double', 'array', 'DateTime'));
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public function postParse(array $input, array $parameters)
|
|
{
|
|
return $this->doPostParse($parameters, array(), isset($input['groups']) ? $input['groups'] : array());
|
|
}
|
|
|
|
/**
|
|
* Recursive `doPostParse` to avoid circular post parsing.
|
|
*
|
|
* @param array $parameters
|
|
* @param array $visited
|
|
* @return array
|
|
*/
|
|
protected function doPostParse (array $parameters, array $visited = array(), array $groups = array())
|
|
{
|
|
foreach ($parameters as $param => $data) {
|
|
if (isset($data['class']) && isset($data['children']) && !in_array($data['class'], $visited)) {
|
|
$visited[] = $data['class'];
|
|
|
|
$input = array('class' => $data['class'], 'groups' => isset($data['groups']) ? $data['groups'] : array());
|
|
$parameters[$param]['children'] = array_merge(
|
|
$parameters[$param]['children'], $this->doPostParse($parameters[$param]['children'], $visited, $groups)
|
|
);
|
|
$parameters[$param]['children'] = array_merge(
|
|
$parameters[$param]['children'], $this->doParse($input['class'], $visited, $groups)
|
|
);
|
|
}
|
|
}
|
|
|
|
return $parameters;
|
|
}
|
|
|
|
/**
|
|
* Check the various ways JMS describes values in arrays, and
|
|
* get the value type in the array
|
|
*
|
|
* @param PropertyMetadata $item
|
|
* @return string|null
|
|
*/
|
|
protected function getNestedTypeInArray(PropertyMetadata $item)
|
|
{
|
|
if (isset($item->type['name']) && in_array($item->type['name'], array('array', 'ArrayCollection'))) {
|
|
if (isset($item->type['params'][1]['name'])) {
|
|
// E.g. array<string, MyNamespaceMyObject>
|
|
return $item->type['params'][1]['name'];
|
|
}
|
|
if (isset($item->type['params'][0]['name'])) {
|
|
// E.g. array<MyNamespaceMyObject>
|
|
return $item->type['params'][0]['name'];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function getDescription(PropertyMetadata $item)
|
|
{
|
|
$ref = new \ReflectionClass($item->class);
|
|
if ($item instanceof VirtualPropertyMetadata) {
|
|
$extracted = $this->commentExtractor->getDocCommentText($ref->getMethod($item->getter));
|
|
} else {
|
|
$extracted = $this->commentExtractor->getDocCommentText($ref->getProperty($item->name));
|
|
}
|
|
|
|
return $extracted;
|
|
}
|
|
}
|