NelmioApiDocBundle/Extractor/ApiDocExtractor.php

369 lines
12 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 Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Parser\ParserInterface;
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-03-26 11:49:12 +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;
/**
* @var array ParserInterface
*/
protected $parsers = array();
/**
* @var array HandlerInterface
*/
protected $handlers;
2013-04-16 13:46:15 +02:00
public function __construct(ContainerInterface $container, RouterInterface $router, Reader $reader, DocCommentExtractor $commentExtractor, array $handlers)
{
2012-04-13 16:33:24 +02:00
$this->container = $container;
$this->router = $router;
$this->reader = $reader;
$this->commentExtractor = $commentExtractor;
$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();
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'))) {
if ($annotation = $this->reader->getMethodAnnotation($method, self::ANNOTATION_CLASS)) {
if ($annotation->isResource()) {
// 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) {
if (0 === strpos($pattern, $resource)) {
$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');
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 = 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
2012-05-23 00:33:01 +02:00
* @param Route $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;
// parse annotations
$this->parseAnnotations($annotation, $route, $method);
// route
$annotation->setRoute($route);
// description
if (null === $annotation->getDescription()) {
$comments = explode("\n", $this->commentExtractor->getDocCommentText($method));
// just set the first line
$comment = trim($comments[0]);
2012-07-13 15:53:24 +02:00
$comment = preg_replace("#\n+#", ' ', $comment);
$comment = preg_replace('#\s+#', ' ', $comment);
$comment = preg_replace('#[_`*]+#', '', $comment);
if ('@' !== substr($comment, 0, 1)) {
$annotation->setDescription($comment);
}
}
// doc
$annotation->setDocumentation($this->commentExtractor->getDocCommentText($method));
2012-08-27 12:56:19 -04:00
// input (populates 'parameters' for the formatters)
if (null !== $input = $annotation->getInput()) {
$parameters = array();
foreach ($this->parsers as $parser) {
if ($parser->supports($input)) {
$parameters = $parser->parse($input);
break;
}
}
if ('PUT' === $method) {
// All parameters are optional with PUT (update)
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()) {
2012-08-27 12:56:19 -04:00
$response = array();
2012-08-27 13:25:03 -04:00
2012-08-27 12:56:19 -04:00
foreach ($this->parsers as $parser) {
if ($parser->supports($output)) {
$response = $parser->parse($output);
2012-08-27 12:56:19 -04:00
break;
}
}
2012-08-27 13:25:03 -04:00
2012-08-27 12:56:19 -04:00
$annotation->setResponse($response);
}
// requirements
$requirements = array();
foreach ($route->getRequirements() as $name => $value) {
if ('_method' !== $name) {
$requirements[$name] = array(
'requirement' => $value,
'dataType' => '',
'description' => '',
);
}
if ('_scheme' == $name) {
$https = ('https' == $value);
$annotation->setHttps($https);
}
}
$paramDocs = array();
foreach (explode("\n", $this->commentExtractor->getDocComment($method)) as $line) {
if (preg_match('{^@param (.+)}', trim($line), $matches)) {
$paramDocs[] = $matches[1];
}
if (preg_match('{^@deprecated\b(.*)}', trim($line), $matches)) {
$annotation->setDeprecated(true);
}
}
2013-04-16 16:16:30 +01:00
$regexp = '{(\w*) *\$%s\b *(.*)}i';
foreach ($route->compile()->getVariables() as $var) {
$found = false;
foreach ($paramDocs as $paramDoc) {
if (preg_match(sprintf($regexp, preg_quote($var)), $paramDoc, $matches)) {
$requirements[$var]['dataType'] = isset($matches[1]) ? $matches[1] : '';
$requirements[$var]['description'] = $matches[2];
if (!isset($requirements[$var]['requirement'])) {
$requirements[$var]['requirement'] = '';
}
$found = true;
break;
}
}
if (!isset($requirements[$var]) && false === $found) {
$requirements[$var] = array('requirement' => '', 'dataType' => '', 'description' => '');
}
}
$annotation->setRequirements($requirements);
return $annotation;
}
/**
* 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);
}
}
}