|
<?php |
|
|
|
namespace AppBundle\Command; |
|
|
|
use Nelmio\ApiDocBundle\Annotation\ApiDoc; |
|
use ReflectionClass; |
|
use ReflectionMethod; |
|
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; |
|
use Symfony\Component\Console\Input\InputInterface; |
|
use Symfony\Component\Console\Input\InputOption; |
|
use Symfony\Component\Console\Output\OutputInterface; |
|
|
|
/** |
|
* Convert from NelmioApiDocbundle to swagger-php annotations |
|
* |
|
* @author David Buchmann <[email protected]> |
|
*/ |
|
class SwaggerDocblockConvertCommand extends ContainerAwareCommand |
|
{ |
|
/** |
|
* Configure command. |
|
* |
|
* @return void |
|
*/ |
|
protected function configure() |
|
{ |
|
$this |
|
->setDescription('') |
|
->setName('api:doc:convert') |
|
; |
|
} |
|
|
|
/** |
|
* Execute command. |
|
* |
|
* @param InputInterface $input Input |
|
* @param OutputInterface $output Output |
|
* |
|
* @return void |
|
*/ |
|
protected function execute(InputInterface $input, OutputInterface $output) |
|
{ |
|
$extractor = $this->getContainer()->get('nelmio_api_doc.extractor.api_doc_extractor'); |
|
$apiDocs = $extractor->extractAnnotations($extractor->getRoutes()); |
|
|
|
foreach ($apiDocs as $annotation) { |
|
/** @var ApiDoc $apiDoc */ |
|
$apiDoc = $annotation['annotation']; |
|
|
|
$refl = $extractor->getReflectionMethod($apiDoc->getRoute()->getDefault('_controller')); |
|
|
|
$this->rewriteClass($refl->getFileName(), $refl, $apiDoc); |
|
} |
|
} |
|
|
|
/** |
|
* Rewrite class with correct apidoc. |
|
* |
|
* @param string $path Full filename |
|
* @param ReflectionMethod $method Name of the function |
|
* @param ApiDoc $apiDoc Apidoc |
|
* |
|
* @return void |
|
*/ |
|
private function rewriteClass($path, ReflectionMethod $method, ApiDoc $apiDoc) |
|
{ |
|
echo "Processing $path::{$method->name}\n"; |
|
$code = file_get_contents($path); |
|
$old = $this->locateNelmioAnnotation($code, $method->name); |
|
|
|
$code = substr_replace($code, $this->renderSwaggerAnnotation($apiDoc, $method), $old['start'], $old['length']); |
|
$code = str_replace('use Nelmio\ApiDocBundle\Annotation\ApiDoc;', 'use Swagger\Annotations as SWG;', $code); |
|
|
|
file_put_contents($path, $code); |
|
} |
|
|
|
/** |
|
* Render new Swagger annotations for a method |
|
* |
|
* @param ApiDoc $apiDoc Apidoc |
|
* @param ReflectionMethod $method The method |
|
* |
|
* @return string |
|
*/ |
|
private function renderSwaggerAnnotation(ApiDoc $apiDoc, ReflectionMethod $method) |
|
{ |
|
$info = $apiDoc->toArray(); |
|
if ($apiDoc->getResource()) { |
|
throw new \RuntimeException('implement me'); |
|
} |
|
$path = str_replace('.{_format}', '', $apiDoc->getRoute()->getPath()); |
|
|
|
$annotation = '@SWG\\'.ucfirst(strtolower($apiDoc->getMethod())).'( |
|
* path="'.$path.'", |
|
* tags={"'.$apiDoc->getSection().'"}, |
|
* summary="'.$this->escapeQuotes($apiDoc->getDescription()).'"'; |
|
|
|
$paramLines = []; |
|
preg_match_all('/@param .*/', $method->getDocComment(), $paramLines); |
|
$paramLines = reset($paramLines); // first entry is the list of all full matches |
|
|
|
foreach ($paramLines as $line) { |
|
if (!$line || false !== strpos($line, '@param Request')) { |
|
continue; |
|
} |
|
|
|
$parts = []; |
|
preg_match('/@param (\w+) /', $line, $parts); |
|
$type = $parts[1]; |
|
|
|
$nameAndDescription = explode(' ', trim(substr($line, strlen('@param string '))), 2); |
|
$annotation .= ', |
|
* @SWG\Parameter( |
|
* name="'.trim($nameAndDescription[0], '$').'", |
|
* in="path", |
|
* description="'.$this->escapeQuotes(trim($nameAndDescription[1])).'", |
|
* required=true, |
|
* type="'.$type.'" |
|
* )'; |
|
} |
|
|
|
foreach ($apiDoc->getFilters() as $name => $parameter) { |
|
$description = array_key_exists('description', $parameter) |
|
? $this->escapeQuotes($parameter['description']) |
|
: 'todo'; |
|
|
|
$annotation .= ', |
|
* @SWG\Parameter( |
|
* name="'.$name.'", |
|
* in="query", |
|
* description="'.$description.'", |
|
* required='.(array_key_exists($name, $apiDoc->getRequirements())?'true':'false').', |
|
* type="'. $this->determineDataType($parameter) .'" |
|
* )'; |
|
} |
|
|
|
// Put parameters for POST requests into formData, as Swagger cannot handle more than one body parameter |
|
$in = 'POST' === $apiDoc->getMethod() |
|
? 'formData' |
|
: 'body'; |
|
|
|
foreach ($apiDoc->getParameters() as $name => $parameter) { |
|
$description = array_key_exists('description', $parameter) |
|
? $this->escapeQuotes($parameter['description']) |
|
: 'todo'; |
|
|
|
$annotation .= ', |
|
* @SWG\Parameter( |
|
* name="'.$name.'", |
|
* in="'.$in.'", |
|
* description="'.$description.'", |
|
* required='.(array_key_exists($name, $apiDoc->getRequirements())?'true':'false').', |
|
* type="'. $this->determineDataType($parameter) .'"'; |
|
|
|
if ('POST' !== $apiDoc->getMethod()) { |
|
$annotation .= ', |
|
* schema=""'; |
|
} |
|
|
|
$annotation .= ' |
|
* )'; |
|
} |
|
|
|
if (array_key_exists('statusCodes', $info)) { |
|
$responses = $info['statusCodes']; |
|
foreach ($responses as $code => $description) { |
|
$responses[$code] = reset($description); |
|
} |
|
} else { |
|
$responses = [200 => 'Returned when successful']; |
|
} |
|
|
|
$schemas = $apiDoc->getResponseMap(); |
|
if (array_key_exists(200, $schemas) && 'array' !== $schemas[200]['class']) { |
|
if (class_exists($schemas[200]['class'])) { |
|
$reflectionClass = new ReflectionClass($schemas[200]['class']); |
|
$schema = $reflectionClass->getShortName(); |
|
|
|
/* this is a hack if you use several model folders and might have name clashes |
|
* the other part of the hack is to specify the definition name in your model |
|
* class like this: |
|
* |
|
* @SWG\Definition(definition="MyPrefix-BlogCollection") |
|
*/ |
|
|
|
if ('AppBundle\Model\Other' === $reflectionClass->getNamespaceName()) { |
|
$schema = 'MyPrefix-'.$schema; |
|
} |
|
} else { |
|
$schema = "TODO {$schemas[200]['class']} does not exist"; |
|
} |
|
} |
|
|
|
foreach ($responses as $code => $description) { |
|
$annotation .= ", |
|
* @SWG\\Response( |
|
* response=\"$code\", |
|
* description=\"{$this->escapeQuotes($description)}\""; |
|
if (200 === $code && isset($schema)) { |
|
$annotation .= ", |
|
* @SWG\\Schema(ref=\"#/definitions/$schema\")"; |
|
} |
|
$annotation .= ' |
|
* )'; |
|
} |
|
|
|
$annotation .= ' |
|
* ) |
|
*'; |
|
|
|
return $annotation; |
|
} |
|
|
|
/** |
|
* Determine start and end of the annotation. |
|
* |
|
* @param string $code Code of the class |
|
* @param string $methodName Method name to find the annotation for |
|
* |
|
* @return array with `start` position and `length`. |
|
*/ |
|
private function locateNelmioAnnotation($code, $methodName) |
|
{ |
|
$position = strpos($code, "tion $methodName("); |
|
if (false === $position) { |
|
throw new \RuntimeException("Method $methodName not found in controller."); |
|
} |
|
|
|
$docstart = strrpos(substr($code, 0, $position), '@ApiDoc'); |
|
if (false === $docstart) { |
|
throw new \RuntimeException("Method $methodName has no @ApiDoc annotation around\n".substr($code, $position-200, 150)); |
|
} |
|
$docend = strpos($code, '* )', $docstart) + 3; |
|
|
|
return [ |
|
'start' => $docstart, |
|
'length' => $docend - $docstart, |
|
]; |
|
} |
|
|
|
/** |
|
* Escape double quotes in texts by duplicating them. |
|
* |
|
* @param string $str String to escape quotes in |
|
* |
|
* @return string |
|
*/ |
|
private function escapeQuotes($str) |
|
{ |
|
$lines = []; |
|
foreach (explode("\n", $str) as $line) { |
|
$lines[] = trim($line, ' *'); |
|
} |
|
|
|
return str_replace('"', '""', implode(' ', $lines)); |
|
} |
|
|
|
/** |
|
* Find the data type from parameters. |
|
* |
|
* Defaults to string and converts "float" to "number". |
|
* |
|
* @param array $parameter The parameter |
|
* |
|
* @return string |
|
*/ |
|
private function determineDataType(array $parameter) |
|
{ |
|
$dataType = isset($parameter['dataType']) ? $parameter['dataType'] : 'string'; |
|
$transform = [ |
|
'float' => 'number', |
|
'datetime' => 'string', |
|
]; |
|
if (array_key_exists($dataType, $transform)) { |
|
$dataType = $transform[$dataType]; |
|
} |
|
|
|
return $dataType; |
|
} |
|
} |