NelmioApiDocBundle/Extractor/ApiDocExtractor.php

564 lines
19 KiB
PHP
Raw Normal View History

<?php
2012-04-13 11:03:05 +02:00
/*
* 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.
*/
2012-04-12 18:37:42 +02:00
namespace Nelmio\ApiDocBundle\Extractor;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Util\ClassUtils;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
use Nelmio\ApiDocBundle\DataTypes;
use Nelmio\ApiDocBundle\Parser\ParserInterface;
use Nelmio\ApiDocBundle\Parser\PostParserInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouterInterface;
2012-04-13 16:33:24 +02:00
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Nelmio\ApiDocBundle\Util\DocCommentExtractor;
class ApiDocExtractor
{
2013-11-14 10:28:42 +01:00
const ANNOTATION_CLASS = 'Nelmio\\ApiDocBundle\\Annotation\\ApiDoc';
2012-04-13 16:33:24 +02:00
/**
* @var ContainerInterface
2012-04-13 16:33:24 +02:00
*/
protected $container;
2012-04-13 16:33:24 +02:00
/**
* @var RouterInterface
*/
protected $router;
/**
* @var Reader
*/
protected $reader;
/**
* @var DocCommentExtractor
*/
private $commentExtractor;
/**
2013-11-14 10:28:42 +01:00
* @var ParserInterface[]
*/
protected $parsers = array();
/**
2013-11-14 10:28:42 +01:00
* @var HandlerInterface[]
*/
protected $handlers;
2013-04-16 13:46:15 +02:00
public function __construct(ContainerInterface $container, RouterInterface $router, Reader $reader, DocCommentExtractor $commentExtractor, array $handlers)
{
2013-11-14 10:28:42 +01:00
$this->container = $container;
$this->router = $router;
$this->reader = $reader;
$this->commentExtractor = $commentExtractor;
2013-11-14 10:28:42 +01:00
$this->handlers = $handlers;
}
/**
* Return a list of route to inspect for ApiDoc annotation
* You can extend this method if you don't want all the routes
* to be included.
*
2013-03-18 08:40:03 +01:00
* @return Route[] An array of routes
*/
public function getRoutes()
{
return $this->router->getRouteCollection()->all();
}
/**
* Extracts annotations from all known routes
*
* @return array
*/
public function all()
{
return $this->extractAnnotations($this->getRoutes());
}
2012-04-12 17:48:21 +02:00
/**
* Returns an array of data where each data is an array with the following keys:
* - annotation
* - resource
*
2013-03-27 23:19:16 +01:00
* @param array $routes array of Route-objects for which the annotations should be extracted
*
2012-04-12 17:48:21 +02:00
* @return array
*/
2013-03-27 23:19:16 +01:00
public function extractAnnotations(array $routes)
{
2013-03-18 08:40:03 +01:00
$array = array();
$resources = array();
$excludeSections = $this->container->getParameter('nelmio_api_doc.exclude_sections');
foreach ($routes as $route) {
2013-03-27 23:21:04 +01:00
if (!$route instanceof Route) {
2013-03-27 22:28:26 +01:00
throw new \InvalidArgumentException(sprintf('All elements of $routes must be instances of Route. "%s" given', gettype($route)));
}
if ($method = $this->getReflectionMethod($route->getDefault('_controller'))) {
$annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS);
if ($annotation && !in_array($annotation->getSection(), $excludeSections)) {
if ($annotation->isResource()) {
2013-10-11 16:40:26 +02:00
if ($resource = $annotation->getResource()) {
$resources[] = $resource;
} else {
2013-10-11 15:44:31 +02:00
// remove format from routes used for resource grouping
$resources[] = str_replace('.{_format}', '', $route->getPattern());
}
2012-04-13 16:33:24 +02:00
}
$array[] = array('annotation' => $this->extractData($annotation, $route, $method));
2012-04-13 16:33:24 +02:00
}
}
}
rsort($resources);
foreach ($array as $index => $element) {
$hasResource = false;
$pattern = $element['annotation']->getRoute()->getPattern();
foreach ($resources as $resource) {
2013-10-11 15:44:31 +02:00
if (0 === strpos($pattern, $resource) || $resource === $element['annotation']->getResource()) {
$array[$index]['resource'] = $resource;
$hasResource = true;
break;
}
}
if (false === $hasResource) {
$array[$index]['resource'] = 'others';
}
}
2012-04-13 10:48:25 +02:00
$methodOrder = array('GET', 'POST', 'PUT', 'DELETE');
2013-11-14 10:28:42 +01:00
usort($array, function ($a, $b) use ($methodOrder) {
if ($a['resource'] === $b['resource']) {
if ($a['annotation']->getRoute()->getPattern() === $b['annotation']->getRoute()->getPattern()) {
$methodA = array_search($a['annotation']->getRoute()->getRequirement('_method'), $methodOrder);
$methodB = array_search($b['annotation']->getRoute()->getRequirement('_method'), $methodOrder);
2012-04-13 10:48:25 +02:00
if ($methodA === $methodB) {
return strcmp(
$a['annotation']->getRoute()->getRequirement('_method'),
$b['annotation']->getRoute()->getRequirement('_method')
);
2012-04-13 10:48:25 +02:00
}
return $methodA > $methodB ? 1 : -1;
}
return strcmp(
$a['annotation']->getRoute()->getPattern(),
$b['annotation']->getRoute()->getPattern()
);
}
return strcmp($a['resource'], $b['resource']);
});
return $array;
}
/**
* Returns the ReflectionMethod for the given controller string.
*
* @param string $controller
2012-05-23 00:20:50 +02:00
* @return \ReflectionMethod|null
*/
public function getReflectionMethod($controller)
{
if (preg_match('#(.+)::([\w]+)#', $controller, $matches)) {
$class = $matches[1];
$method = $matches[2];
} elseif (preg_match('#(.+):([\w]+)#', $controller, $matches)) {
$controller = $matches[1];
$method = $matches[2];
if ($this->container->has($controller)) {
$this->container->enterScope('request');
2013-05-03 16:26:16 +02:00
$this->container->set('request', new Request(), 'request');
$class = ClassUtils::getRealClass(get_class($this->container->get($controller)));
$this->container->leaveScope('request');
}
}
if (isset($class) && isset($method)) {
try {
return new \ReflectionMethod($class, $method);
} catch (\ReflectionException $e) {
}
}
return null;
}
2012-04-12 17:48:21 +02:00
/**
* Returns an ApiDoc annotation.
2012-04-12 17:48:21 +02:00
*
* @param string $controller
* @param string $route
* @return ApiDoc|null
2012-04-12 17:48:21 +02:00
*/
public function get($controller, $route)
{
if ($method = $this->getReflectionMethod($controller)) {
if ($annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS)) {
if ($route = $this->router->getRouteCollection()->get($route)) {
return $this->extractData($annotation, $route, $method);
}
}
}
2012-04-13 14:42:28 +02:00
return null;
}
/**
* Registers a class parser to use for parsing input class metadata
*
* @param ParserInterface $parser
*/
public function addParser(ParserInterface $parser)
{
$this->parsers[] = $parser;
}
/**
* Returns a new ApiDoc instance with more data.
*
2012-05-23 00:33:01 +02:00
* @param ApiDoc $annotation
* @param Route $route
* @param \ReflectionMethod $method
* @return ApiDoc
*/
protected function extractData(ApiDoc $annotation, Route $route, \ReflectionMethod $method)
{
// create a new annotation
$annotation = clone $annotation;
// doc
$annotation->setDocumentation($this->commentExtractor->getDocCommentText($method));
// parse annotations
$this->parseAnnotations($annotation, $route, $method);
// route
$annotation->setRoute($route);
2012-08-27 12:56:19 -04:00
// input (populates 'parameters' for the formatters)
if (null !== $input = $annotation->getInput()) {
2013-12-05 00:05:47 +01:00
$parameters = array();
$normalizedInput = $this->normalizeClassParameter($input);
$supportedParsers = array();
2013-12-05 00:05:47 +01:00
foreach ($this->getParsers($normalizedInput) as $parser) {
if ($parser->supports($normalizedInput)) {
$supportedParsers[] = $parser;
2013-12-05 00:05:47 +01:00
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedInput));
}
}
2013-11-14 10:28:42 +01:00
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
2013-12-05 00:05:47 +01:00
$parameters = $this->mergeParameters(
$parameters,
$parser->postParse($normalizedInput, $parameters)
);
}
}
$parameters = $this->clearClasses($parameters);
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
$parameters = $this->generateHumanReadableTypes($parameters);
if ('PUT' === $method) {
// All parameters are optional with PUT (update)
2013-11-14 10:28:42 +01:00
array_walk($parameters, function ($val, $key) use (&$data) {
$parameters[$key]['required'] = false;
});
}
$annotation->setParameters($parameters);
}
// output (populates 'response' for the formatters)
if (null !== $output = $annotation->getOutput()) {
2013-12-05 00:05:47 +01:00
$response = array();
$supportedParsers = array();
$normalizedOutput = $this->normalizeClassParameter($output);
2013-12-05 00:05:47 +01:00
foreach ($this->getParsers($normalizedOutput) as $parser) {
if ($parser->supports($normalizedOutput)) {
$supportedParsers[] = $parser;
2013-11-07 13:30:13 +01:00
$response = $this->mergeParameters($response, $parser->parse($normalizedOutput));
2012-08-27 12:56:19 -04:00
}
}
2013-12-05 00:05:47 +01:00
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$mp = $parser->postParse($normalizedOutput, $response);
$response = $this->mergeParameters($response, $mp);
}
}
$response = $this->clearClasses($response);
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
$response = $this->generateHumanReadableTypes($response);
2012-08-27 13:25:03 -04:00
2012-08-27 12:56:19 -04:00
$annotation->setResponse($response);
$annotation->setResponseForStatusCode($response, $normalizedOutput, 200);
}
if (count($annotation->getResponseMap()) > 0) {
foreach ($annotation->getResponseMap() as $code => $modelName) {
if ('200' === (string) $code && isset($modelName['type']) && isset($modelName['model'])) {
/*
* Model was already parsed as the default `output` for this ApiDoc.
*/
continue;
}
$normalizedModel = $this->normalizeClassParameter($modelName);
$parameters = array();
$supportedParsers = array();
foreach ($this->getParsers($normalizedModel) as $parser) {
if ($parser->supports($normalizedModel)) {
$supportedParsers[] = $parser;
$parameters = $this->mergeParameters($parameters, $parser->parse($normalizedModel));
}
}
foreach ($supportedParsers as $parser) {
if ($parser instanceof PostParserInterface) {
$mp = $parser->postParse($normalizedModel, $parameters);
$parameters = $this->mergeParameters($parameters, $mp);
}
}
$parameters = $this->clearClasses($parameters);
$parameters = $this->generateHumanReadableTypes($parameters);
$annotation->setResponseForStatusCode($parameters, $normalizedModel, $code);
}
2012-08-27 12:56:19 -04:00
}
return $annotation;
}
protected function normalizeClassParameter($input)
{
$defaults = array(
'class' => '',
'groups' => array(),
);
// normalize strings
if (is_string($input)) {
$input = array('class' => $input);
}
2014-08-07 17:02:15 -07:00
$collectionData = array();
preg_match_all("/array<(.*)>( as (.*))?/", $input['class'], $collectionData);
if (count($collectionData[0]) > 0) {
$input['class'] = $collectionData[1][0];
$input['collection'] = true;
$input['collectionName'] = $collectionData[3][0];
}
// normalize groups
if (isset($input['groups']) && is_string($input['groups'])) {
$input['groups'] = array_map('trim', explode(',', $input['groups']));
}
return array_merge($defaults, $input);
}
/**
* Merges two parameter arrays together. This logic allows documentation to be built
* from multiple parser passes, with later passes extending previous passes:
* - Boolean values are true if any parser returns true.
* - Requirement parameters are concatenated.
* - Other string values are overridden by later parsers when present.
* - Array parameters are recursively merged.
* - Non-null default values prevail over null default values. Later values overrides previous defaults.
*
* However, if newly-returned parameter array contains a parameter with NULL, the parameter is removed from the merged results.
* If the parameter is not present in the newly-returned array, then it is left as-is.
*
2013-11-14 10:28:42 +01:00
* @param array $p1 The pre-existing parameters array.
* @param array $p2 The newly-returned parameters array.
* @return array The resulting, merged array.
*/
protected function mergeParameters($p1, $p2)
{
$params = $p1;
2013-11-14 10:28:42 +01:00
foreach ($p2 as $propname => $propvalue) {
if ($propvalue === null) {
unset($params[$propname]);
continue;
}
2013-11-14 10:28:42 +01:00
if (!isset($p1[$propname])) {
$params[$propname] = $propvalue;
} elseif (is_array($propvalue)) {
$v1 = $p1[$propname];
2013-11-14 10:28:42 +01:00
foreach ($propvalue as $name => $value) {
if (is_array($value)) {
if (isset($v1[$name]) && is_array($v1[$name])) {
$v1[$name] = $this->mergeParameters($v1[$name], $value);
} else {
$v1[$name] = $value;
}
2013-11-14 10:28:42 +01:00
} elseif (!is_null($value)) {
if (in_array($name, array('required', 'readonly'))) {
$v1[$name] = $v1[$name] || $value;
2013-11-14 10:28:42 +01:00
} elseif (in_array($name, array('requirement'))) {
if (isset($v1[$name])) {
$v1[$name] .= ', ' . $value;
} else {
$v1[$name] = $value;
}
} elseif ($name == 'default') {
$v1[$name] = $value ?: $v1[$name];
} else {
$v1[$name] = $value;
}
}
}
$params[$propname] = $v1;
}
}
return $params;
}
/**
* Parses annotations for a given method, and adds new information to the given ApiDoc
* annotation. Useful to extract information from the FOSRestBundle annotations.
*
* @param ApiDoc $annotation
* @param Route $route
* @param ReflectionMethod $method
*/
protected function parseAnnotations(ApiDoc $annotation, Route $route, \ReflectionMethod $method)
{
$annots = $this->reader->getMethodAnnotations($method);
foreach ($this->handlers as $handler) {
2013-04-16 13:46:15 +02:00
$handler->handle($annotation, $annots, $route, $method);
}
}
/**
* Clears the temporary 'class' parameter from the parameters array before it is returned.
*
2013-11-14 10:28:42 +01:00
* @param array $array The source array.
* @return array The cleared array.
*/
protected function clearClasses($array)
{
2013-11-14 10:28:42 +01:00
if (is_array($array)) {
unset($array['class']);
2013-11-14 10:28:42 +01:00
foreach ($array as $name => $item) {
$array[$name] = $this->clearClasses($item);
}
}
2013-11-14 10:28:42 +01:00
return $array;
}
2013-12-05 00:05:47 +01:00
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
/**
* Populates the `dataType` properties in the parameter array if empty. Recurses through children when necessary.
*
* @param array $array
* @return array
*/
protected function generateHumanReadableTypes(array $array)
{
foreach ($array as $name => $info) {
if (empty($info['dataType'])) {
$array[$name]['dataType'] = $this->generateHumanReadableType($info['actualType'], $info['subType']);
}
if (isset($info['children'])) {
$array[$name]['children'] = $this->generateHumanReadableTypes($info['children']);
}
}
return $array;
}
/**
* Creates a human-readable version of the `actualType`. `subType` is taken into account.
*
* @param string $actualType
* @param string $subType
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
* @return string
*/
protected function generateHumanReadableType($actualType, $subType)
{
if ($actualType == DataTypes::MODEL) {
if (class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('object (%s)', end($parts));
}
return sprintf('object (%s)', $subType);
Unified data types [actualType and subType] This is the result of https://github.com/nelmio/NelmioApiDocBundle/issues/410. This PR aims to provide a uniform way of declaring data-types of parameters for parsers and handlers to follow. In turn, this would allow formatters to determine data-types in a cleaner and less volatile manner. (See use-case that can be improved with this PR: https://github.com/nelmio/NelmioApiDocBundle/blob/master/Formatter/AbstractFormatter.php#L103) This is possible by the addition two properties to each property item in `response`, and `parameters` fields in each API endpoint produced by the `ApiDocExtractor`: * `actualType` Contains a value from one of the `DataTypes` class constants. * `subType` Can contain either `null`, or any other `DataTypes` class constant. This is relevant when the `actualType` is a `DataTypes::COLLECTION`, wherein `subType` would specify the type of the collection items. It is also relevant when `actualType` is a `DataTypes::MODEL`, wherein `subType` would contain an identifier of the model (the FQCN or anything the parser would wish to specify) Examples: ```php array( 'id' => array( 'dataType' => 'integer', 'actualType' => DataTypes::INTEGER, 'subType' => null, ), 'profile' => array( 'dataType' => 'object (Profile)', 'actualType' => DataTypes::MODEL, 'subType' => 'Foo\Entity\Profile', 'children' => array( 'name' => array( 'dataType' => 'string', 'actualType' => DataTypes::STRING, 'subType' => null, ), 'birthDate' => array( 'dataType' => 'date', 'actualType' => DataTypes::DATE, 'subType' => null, ), ) ), 'languages' => array( 'dataType' => 'array of strings', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::STRING, ), 'roles' => array( 'dataType' => 'array of choices', 'actualType' => DataTypes::COLLECTION, 'subType' => DataTypes::ENUM, ), 'groups' => array( 'dataType' => 'array of objects (Group)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Group', ), 'profileRevisions' => array( 'dataType' => 'array of objects (Profile)', 'actualType' => DataTypes::COLLECTION, 'subType' => 'Foo\Entity\Profile', ), 'address' => array( 'dataType' => 'object (a_type_a_custom_JMS_serializer_handler_handles)', 'actualType' => DataTypes::MODEL, 'subType' => 'a_type_a_custom_JMS_serializer_handler_handles', ), ); ``` When a formatter omits the `dataType` property or leaves it blank, it is inferred within `ApiDocExtractor` before everything is passed to formatters.
2014-06-17 17:05:00 -07:00
}
if ($actualType == DataTypes::COLLECTION) {
if (DataTypes::isPrimitive($subType)) {
return sprintf('array of %ss', $subType);
}
if (class_exists($subType)) {
$parts = explode('\\', $subType);
return sprintf('array of objects (%s)', end($parts));
}
return sprintf('array of objects (%s)', $subType);
}
return $actualType;
}
2013-12-05 00:05:47 +01:00
private function getParsers(array $parameters)
{
if (isset($parameters['parsers'])) {
$parsers = array();
foreach ($this->parsers as $parser) {
if (in_array(get_class($parser), $parameters['parsers'])) {
$parsers[] = $parser;
}
}
} else {
$parsers = $this->parsers;
}
return $parsers;
}
}