Skip to content

Instantly share code, notes, and snippets.

@dbu
Last active Oct 2, 2018
Embed
What would you like to do?
Convert NelmioApiDocBundle annotations to Swagger PHP

A Symfony command to convert from NelmioApiDocBundle annotations to Swagger-PHP annotations.

This code is provided as is. Make sure to have your code committed to version control before running the command. Check if things work out and if not, use version control to reset the automated changes and fix the command.

<?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 <david@liip.ch>
*/
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment