2016-07-28 10:20:59 +02:00
< ? php
/*
2016-12-29 12:09:26 +01:00
* This file is part of the NelmioApiDocBundle package .
2016-07-28 10:20:59 +02:00
*
2016-12-29 12:09:26 +01:00
* ( c ) Nelmio
2016-07-28 10:20:59 +02:00
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
2016-12-29 12:09:26 +01:00
namespace Nelmio\ApiDocBundle\Describer ;
2016-07-28 10:20:59 +02:00
2017-03-16 19:35:04 +01:00
use Doctrine\Common\Annotations\Reader ;
use EXSyst\Component\Swagger\Swagger ;
use Nelmio\ApiDocBundle\Annotation\Operation ;
2018-01-25 21:11:34 +01:00
use Nelmio\ApiDocBundle\Annotation\Security ;
2017-01-14 17:36:56 +01:00
use Nelmio\ApiDocBundle\SwaggerPhp\AddDefaults ;
use Nelmio\ApiDocBundle\SwaggerPhp\ModelRegister ;
2017-01-23 19:46:38 +01:00
use Nelmio\ApiDocBundle\Util\ControllerReflector ;
2018-04-20 10:34:55 +02:00
use Psr\Log\LoggerInterface ;
2016-12-30 13:37:02 +01:00
use Swagger\Analysis ;
2017-06-26 10:34:42 +02:00
use Swagger\Annotations\AbstractAnnotation ;
2017-03-16 19:35:04 +01:00
use Swagger\Annotations as SWG ;
use Swagger\Context ;
2017-01-23 19:46:38 +01:00
use Symfony\Component\Routing\RouteCollection ;
2016-12-30 13:37:02 +01:00
2020-04-28 17:40:51 +03:00
// Help opcache.preload discover Swagger\Annotations\Swagger
class_exists ( SWG\Swagger :: class );
2018-02-19 10:49:52 +01:00
final class SwaggerPhpDescriber implements ModelRegistryAwareInterface
2016-07-28 10:20:59 +02:00
{
2017-01-14 17:36:56 +01:00
use ModelRegistryAwareTrait ;
2017-01-23 19:46:38 +01:00
private $routeCollection ;
private $controllerReflector ;
2017-03-16 19:35:04 +01:00
private $annotationReader ;
2018-04-20 10:34:55 +02:00
private $logger ;
2018-02-19 10:49:52 +01:00
private $overwrite ;
2016-12-30 13:37:02 +01:00
2018-04-20 10:34:55 +02:00
public function __construct ( RouteCollection $routeCollection , ControllerReflector $controllerReflector , Reader $annotationReader , LoggerInterface $logger , bool $overwrite = false )
2016-07-28 10:20:59 +02:00
{
2017-01-23 19:46:38 +01:00
$this -> routeCollection = $routeCollection ;
$this -> controllerReflector = $controllerReflector ;
2017-03-16 19:35:04 +01:00
$this -> annotationReader = $annotationReader ;
2018-04-20 10:34:55 +02:00
$this -> logger = $logger ;
2018-02-19 10:49:52 +01:00
$this -> overwrite = $overwrite ;
}
2017-01-14 17:36:56 +01:00
2018-02-19 10:49:52 +01:00
public function describe ( Swagger $api )
{
$analysis = $this -> getAnnotations ( $api );
2017-03-16 19:35:04 +01:00
2018-02-19 10:49:52 +01:00
$analysis -> process ( $this -> getProcessors ());
$analysis -> validate ();
2017-03-16 19:35:04 +01:00
2018-02-19 10:49:52 +01:00
$api -> merge ( json_decode ( json_encode ( $analysis -> swagger ), true ), $this -> overwrite );
2016-07-28 10:20:59 +02:00
}
2016-12-30 13:37:02 +01:00
2017-03-16 19:35:04 +01:00
private function getProcessors () : array
{
$processors = [
new AddDefaults (),
new ModelRegister ( $this -> modelRegistry ),
];
return array_merge ( $processors , Analysis :: processors ());
}
2018-02-19 10:49:52 +01:00
private function getAnnotations ( Swagger $api ) : Analysis
2017-03-16 19:35:04 +01:00
{
$analysis = new Analysis ();
2018-02-19 10:49:52 +01:00
$analysis -> addAnnotation ( new class ( $api ) extends SWG\Swagger {
private $api ;
public function __construct ( Swagger $api )
{
$this -> api = $api ;
parent :: __construct ([]);
}
/**
* Support definitions from the config and reference to models .
*/
public function ref ( $ref )
{
if ( 0 === strpos ( $ref , '#/definitions/' ) && $this -> api -> getDefinitions () -> has ( substr ( $ref , 14 ))) {
return ;
}
2018-05-10 18:46:48 +02:00
if ( 0 === strpos ( $ref , '#/parameters/' ) && isset ( $this -> api -> getParameters ()[ substr ( $ref , 13 )])) {
return ;
}
2018-10-02 16:06:18 +07:00
if ( 0 === strpos ( $ref , '#/responses/' ) && $this -> api -> getResponses () -> has ( substr ( $ref , 12 ))) {
return ;
}
2018-02-19 10:49:52 +01:00
parent :: ref ( $ref );
}
}, null );
2017-03-16 19:35:04 +01:00
$operationAnnotations = [
'get' => SWG\Get :: class ,
'post' => SWG\Post :: class ,
'put' => SWG\Put :: class ,
'patch' => SWG\Patch :: class ,
'delete' => SWG\Delete :: class ,
'options' => SWG\Options :: class ,
'head' => SWG\Head :: class ,
];
2018-04-16 13:43:42 +02:00
$classAnnotations = [];
2017-03-16 19:35:04 +01:00
foreach ( $this -> getMethodsToParse () as $method => list ( $path , $httpMethods )) {
2018-04-16 13:43:42 +02:00
$declaringClass = $method -> getDeclaringClass ();
if ( ! array_key_exists ( $declaringClass -> getName (), $classAnnotations )) {
$classAnnotations = array_filter ( $this -> annotationReader -> getClassAnnotations ( $declaringClass ), function ( $v ) {
return $v instanceof SWG\AbstractAnnotation ;
});
$classAnnotations [ $declaringClass -> getName ()] = $classAnnotations ;
}
2017-03-16 19:35:04 +01:00
$annotations = array_filter ( $this -> annotationReader -> getMethodAnnotations ( $method ), function ( $v ) {
return $v instanceof SWG\AbstractAnnotation ;
});
2020-07-11 17:53:09 +02:00
if ( 0 === count ( $annotations ) && 0 === count ( $classAnnotations [ $declaringClass -> getName ()])) {
2017-03-16 19:35:04 +01:00
continue ;
}
$context = new Context ([
'namespace' => $method -> getNamespaceName (),
'class' => $declaringClass -> getShortName (),
'method' => $method -> name ,
'filename' => $method -> getFileName (),
]);
2017-06-02 21:30:31 +02:00
$nestedContext = clone $context ;
$nestedContext -> nested = true ;
2017-03-16 19:35:04 +01:00
$implicitAnnotations = [];
2018-06-02 13:48:44 +02:00
$operations = [];
2017-03-17 19:45:46 +01:00
$tags = [];
2018-01-25 21:11:34 +01:00
$security = [];
2018-04-16 13:43:42 +02:00
foreach ( array_merge ( $annotations , $classAnnotations [ $declaringClass -> getName ()]) as $annotation ) {
2017-03-16 19:35:04 +01:00
$annotation -> _context = $context ;
2017-06-02 21:30:31 +02:00
$this -> updateNestedAnnotations ( $annotation , $nestedContext );
2017-03-16 19:35:04 +01:00
if ( $annotation instanceof Operation ) {
foreach ( $httpMethods as $httpMethod ) {
$annotationClass = $operationAnnotations [ $httpMethod ];
$operation = new $annotationClass ([ '_context' => $context ]);
$operation -> path = $path ;
$operation -> mergeProperties ( $annotation );
2018-06-02 13:48:44 +02:00
$operations [ $httpMethod ] = $operation ;
2017-03-16 19:35:04 +01:00
$analysis -> addAnnotation ( $operation , null );
}
continue ;
}
if ( $annotation instanceof SWG\Operation ) {
if ( null === $annotation -> path ) {
$annotation = clone $annotation ;
$annotation -> path = $path ;
}
2018-06-02 13:48:44 +02:00
$operations [ $annotation -> method ] = $annotation ;
2017-03-16 19:35:04 +01:00
$analysis -> addAnnotation ( $annotation , null );
continue ;
}
2018-01-25 21:11:34 +01:00
if ( $annotation instanceof Security ) {
$annotation -> validate ();
$security [] = [ $annotation -> name => []];
continue ;
}
2017-03-17 19:45:46 +01:00
if ( $annotation instanceof SWG\Tag ) {
$annotation -> validate ();
$tags [] = $annotation -> name ;
continue ;
}
2017-03-16 19:35:04 +01:00
if ( ! $annotation instanceof SWG\Response && ! $annotation instanceof SWG\Parameter && ! $annotation instanceof SWG\ExternalDocumentation ) {
2019-04-12 09:35:49 +02:00
throw new \LogicException ( sprintf ( 'Using the annotation "%s" as a root annotation in "%s::%s()" is not allowed. It should probably be nested in a `@SWG\Response` or `@SWG\Parameter` annotation.' , get_class ( $annotation ), $method -> getDeclaringClass () -> name , $method -> name ));
2017-03-16 19:35:04 +01:00
}
$implicitAnnotations [] = $annotation ;
}
2018-01-25 21:11:34 +01:00
if ( 0 === count ( $implicitAnnotations ) && 0 === count ( $tags ) && 0 === count ( $security )) {
2017-03-16 19:35:04 +01:00
continue ;
}
2018-06-02 13:48:44 +02:00
// Registers new annotations
$analysis -> addAnnotations ( $implicitAnnotations , null );
2017-03-16 19:35:04 +01:00
foreach ( $httpMethods as $httpMethod ) {
$annotationClass = $operationAnnotations [ $httpMethod ];
2018-01-25 21:11:34 +01:00
$constructorArg = [
'_context' => $context ,
'path' => $path ,
'value' => $implicitAnnotations ,
];
if ( 0 !== count ( $tags )) {
$constructorArg [ 'tags' ] = $tags ;
}
if ( 0 !== count ( $security )) {
$constructorArg [ 'security' ] = $security ;
}
$operation = new $annotationClass ( $constructorArg );
2018-06-02 13:48:44 +02:00
if ( isset ( $operations [ $httpMethod ])) {
$operations [ $httpMethod ] -> mergeProperties ( $operation );
} else {
$analysis -> addAnnotation ( $operation , null );
}
2017-03-16 19:35:04 +01:00
}
}
return $analysis ;
}
2017-03-17 19:37:41 +01:00
private function getMethodsToParse () : \Generator
2016-12-30 13:37:02 +01:00
{
2017-01-23 19:46:38 +01:00
foreach ( $this -> routeCollection -> all () as $route ) {
if ( ! $route -> hasDefault ( '_controller' )) {
continue ;
}
$controller = $route -> getDefault ( '_controller' );
2019-11-19 10:28:11 +01:00
if ( $method = $this -> controllerReflector -> getReflectionMethod ( $controller )) {
2017-03-16 19:35:04 +01:00
$path = $this -> normalizePath ( $route -> getPath ());
$httpMethods = $route -> getMethods () ? : Swagger :: $METHODS ;
$httpMethods = array_map ( 'strtolower' , $httpMethods );
2018-04-20 17:24:28 +02:00
$supportedHttpMethods = array_intersect ( $httpMethods , Swagger :: $METHODS );
2017-01-23 19:46:38 +01:00
2018-04-20 17:24:28 +02:00
if ( empty ( $supportedHttpMethods )) {
$this -> logger -> warning ( 'None of the HTTP methods specified for path {path} are supported by swagger-ui, skipping this path' , [
2018-04-20 10:34:55 +02:00
'path' => $path ,
'methods' => $httpMethods ,
]);
2017-01-23 19:46:38 +01:00
2018-04-19 09:51:52 +02:00
continue ;
}
2018-04-20 17:24:28 +02:00
yield $method => [ $path , $supportedHttpMethods ];
2017-01-23 19:46:38 +01:00
}
}
2016-12-30 13:37:02 +01:00
}
2017-01-14 17:36:56 +01:00
2017-03-17 19:37:41 +01:00
private function normalizePath ( string $path ) : string
2017-01-14 17:36:56 +01:00
{
2017-12-17 10:44:07 +01:00
if ( '.{_format}' === substr ( $path , - 10 )) {
2017-03-16 19:35:04 +01:00
$path = substr ( $path , 0 , - 10 );
}
2017-01-14 17:36:56 +01:00
2017-03-16 19:35:04 +01:00
return $path ;
2017-01-14 17:36:56 +01:00
}
2017-06-02 21:30:31 +02:00
2017-06-26 10:34:42 +02:00
private function updateNestedAnnotations ( $value , Context $context )
{
2017-06-02 21:30:31 +02:00
if ( $value instanceof AbstractAnnotation ) {
$value -> _context = $context ;
} elseif ( ! is_array ( $value )) {
return ;
}
foreach ( $value as $v ) {
$this -> updateNestedAnnotations ( $v , $context );
}
}
2016-07-28 10:20:59 +02:00
}