|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +/* |
| 6 | + * This file is part of the Sonata Project package. |
| 7 | + * |
| 8 | + * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org> |
| 9 | + * |
| 10 | + * For the full copyright and license information, please view the LICENSE |
| 11 | + * file that was distributed with this source code. |
| 12 | + */ |
| 13 | + |
| 14 | +namespace Sonata\NewsBundle\Command; |
| 15 | + |
| 16 | +use Nelmio\ApiDocBundle\Annotation\ApiDoc; |
| 17 | +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; |
| 18 | +use Symfony\Component\Console\Input\InputInterface; |
| 19 | +use Symfony\Component\Console\Input\InputOption; |
| 20 | +use Symfony\Component\Console\Output\OutputInterface; |
| 21 | + |
| 22 | +/** |
| 23 | + * Converts ApiDoc annotations to Swagger-PHP annotations. |
| 24 | + * |
| 25 | + * @author David Buchmann <david@liip.ch> |
| 26 | + * @author Guilhem Niot <guilhem.niot@gmail.com> |
| 27 | + */ |
| 28 | +class SwaggerDocblockConvertCommand extends ContainerAwareCommand |
| 29 | +{ |
| 30 | + protected function configure() |
| 31 | + { |
| 32 | + $this |
| 33 | + ->setDescription('') |
| 34 | + ->setName('api:doc:convert') |
| 35 | + ->addOption('views', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of views to convert the documentation for', 'default') |
| 36 | + ; |
| 37 | + } |
| 38 | + |
| 39 | + protected function execute(InputInterface $input, OutputInterface $output) |
| 40 | + { |
| 41 | + $views = explode(',', $input->getOption('views')); |
| 42 | + |
| 43 | + if (!$this->getContainer()->has('nelmio_api_doc.extractor.api_doc_extractor')) { |
| 44 | + if (!$this->getContainer()->has('nelmio_api_doc.controller.swagger_ui')) { |
| 45 | + throw new \RuntimeException('NelmioApiDocBundle is not installed. Please run `composer require nelmio/api-doc-bundle`.'); |
| 46 | + } |
| 47 | + throw new \RuntimeException('This command only works with NelmioApiDocBundle 2.x installed while version 3.x is currently installed. Please downgrade to 2.x to execute this command and bump your constraint only after its execution.'); |
| 48 | + } |
| 49 | + |
| 50 | + $extractor = $this->getContainer()->get('nelmio_api_doc.extractor.api_doc_extractor'); |
| 51 | + |
| 52 | + $apiDocs = []; |
| 53 | + foreach ($views as $view) { |
| 54 | + $apiDocs = array_merge($apiDocs, $extractor->extractAnnotations($extractor->getRoutes(), $view)); |
| 55 | + } |
| 56 | + |
| 57 | + foreach ($apiDocs as $annotation) { |
| 58 | + /** @var ApiDoc $apiDoc */ |
| 59 | + $apiDoc = $annotation['annotation']; |
| 60 | + |
| 61 | + $refl = $extractor->getReflectionMethod($apiDoc->getRoute()->getDefault('_controller')); |
| 62 | + |
| 63 | + $this->rewriteClass($refl->getFileName(), $refl, $apiDoc); |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + /** |
| 68 | + * Rewrite class with correct apidoc. |
| 69 | + */ |
| 70 | + private function rewriteClass(string $path, \ReflectionMethod $method, ApiDoc $apiDoc) |
| 71 | + { |
| 72 | + echo "Processing $path::{$method->name}\n"; |
| 73 | + $code = file_get_contents($path); |
| 74 | + $old = $this->locateNelmioAnnotation($code, $method->name); |
| 75 | + |
| 76 | + $code = substr_replace($code, $this->renderSwaggerAnnotation($apiDoc, $method), $old['start'], $old['length']); |
| 77 | + $code = str_replace('use Nelmio\ApiDocBundle\Annotation\ApiDoc;', "use Nelmio\ApiDocBundle\Annotation\Operation;\nuse Nelmio\ApiDocBundle\Annotation\Model;\nuse Swagger\Annotations as SWG;", $code); |
| 78 | + |
| 79 | + file_put_contents($path, $code); |
| 80 | + } |
| 81 | + |
| 82 | + private function renderSwaggerAnnotation(ApiDoc $apiDoc, \ReflectionMethod $method): string |
| 83 | + { |
| 84 | + $info = $apiDoc->toArray(); |
| 85 | + if ($apiDoc->getResource()) { |
| 86 | + throw new \RuntimeException('implement me'); |
| 87 | + } |
| 88 | + $path = str_replace('.{_format}', '', $apiDoc->getRoute()->getPath()); |
| 89 | + |
| 90 | + $annotation = '@Operation( |
| 91 | + * tags={"'.$apiDoc->getSection().'"}, |
| 92 | + * summary="'.$this->escapeQuotes($apiDoc->getDescription()).'"'; |
| 93 | + |
| 94 | + foreach ($apiDoc->getFilters() as $name => $parameter) { |
| 95 | + $description = \array_key_exists('description', $parameter) && null !== $parameter['description'] |
| 96 | + ? $this->escapeQuotes($parameter['description']) |
| 97 | + : 'todo'; |
| 98 | + |
| 99 | + $annotation .= ', |
| 100 | + * @SWG\Parameter( |
| 101 | + * name="'.$name.'", |
| 102 | + * in="query", |
| 103 | + * description="'.$description.'", |
| 104 | + * required='.(\array_key_exists($name, $apiDoc->getRequirements()) ? 'true' : 'false').', |
| 105 | + * type="'.$this->determineDataType($parameter).'" |
| 106 | + * )'; |
| 107 | + } |
| 108 | + |
| 109 | + // Put parameters for POST requests into formData, as Swagger cannot handle more than one body parameter |
| 110 | + $in = 'POST' === $apiDoc->getMethod() |
| 111 | + ? 'formData' |
| 112 | + : 'body'; |
| 113 | + |
| 114 | + foreach ($apiDoc->getParameters() as $name => $parameter) { |
| 115 | + $description = \array_key_exists('description', $parameter) |
| 116 | + ? $this->escapeQuotes($parameter['description']) |
| 117 | + : 'todo'; |
| 118 | + |
| 119 | + $annotation .= ', |
| 120 | + * @SWG\Parameter( |
| 121 | + * name="'.$name.'", |
| 122 | + * in="'.$in.'", |
| 123 | + * description="'.$description.'", |
| 124 | + * required='.(\array_key_exists($name, $apiDoc->getRequirements()) ? 'true' : 'false'); |
| 125 | + |
| 126 | + if ('POST' !== $apiDoc->getMethod()) { |
| 127 | + $annotation .= ', |
| 128 | + * @SWG\Schema(type="'.$this->determineDataType($parameter).'")'; |
| 129 | + } else { |
| 130 | + $annotation .= ', |
| 131 | + * type="'.$this->determineDataType($parameter).'"'; |
| 132 | + } |
| 133 | + |
| 134 | + $annotation .= ' |
| 135 | + * )'; |
| 136 | + } |
| 137 | + |
| 138 | + if (\array_key_exists('statusCodes', $info)) { |
| 139 | + $responses = $info['statusCodes']; |
| 140 | + foreach ($responses as $code => $description) { |
| 141 | + $responses[$code] = reset($description); |
| 142 | + } |
| 143 | + } else { |
| 144 | + $responses = [200 => 'Returned when successful']; |
| 145 | + } |
| 146 | + |
| 147 | + $responseMap = $apiDoc->getResponseMap(); |
| 148 | + foreach ($responses as $code => $description) { |
| 149 | + $annotation .= ", |
| 150 | + * @SWG\\Response( |
| 151 | + * response=\"$code\", |
| 152 | + * description=\"{$this->escapeQuotes($description)}\""; |
| 153 | + if (200 === $code && isset($responseMap[$code]['class'])) { |
| 154 | + $model = $responseMap[$code]['class']; |
| 155 | + $annotation .= ", |
| 156 | + * @SWG\\Schema(ref=@Model(type=\"$model\"))"; |
| 157 | + } |
| 158 | + $annotation .= ' |
| 159 | + * )'; |
| 160 | + } |
| 161 | + |
| 162 | + $annotation .= ' |
| 163 | + * ) |
| 164 | + *'; |
| 165 | + |
| 166 | + return $annotation; |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * @return array with `start` position and `length` |
| 171 | + */ |
| 172 | + private function locateNelmioAnnotation(string $code, string $methodName): array |
| 173 | + { |
| 174 | + $position = strpos($code, "tion $methodName("); |
| 175 | + if (false === $position) { |
| 176 | + throw new \RuntimeException("Method $methodName not found in controller."); |
| 177 | + } |
| 178 | + |
| 179 | + $docstart = strrpos(substr($code, 0, $position), '@ApiDoc'); |
| 180 | + if (false === $docstart) { |
| 181 | + //If action is defined more than once. Should continue and don't throw exception |
| 182 | + $docstart = strrpos(substr($code, 0, $position), '@Operation'); |
| 183 | + if (false === $docstart) { |
| 184 | + throw new \RuntimeException("Method $methodName has no @ApiDoc annotation around\n".substr($code, $position - 200, 150)); |
| 185 | + } |
| 186 | + } |
| 187 | + $docend = strpos($code, '* )', $docstart) + 3; |
| 188 | + |
| 189 | + return [ |
| 190 | + 'start' => $docstart, |
| 191 | + 'length' => $docend - $docstart, |
| 192 | + ]; |
| 193 | + } |
| 194 | + |
| 195 | + private function escapeQuotes(?string $str = null): string |
| 196 | + { |
| 197 | + if (null === $str) { |
| 198 | + return ''; |
| 199 | + } |
| 200 | + $lines = []; |
| 201 | + foreach (explode("\n", $str) as $line) { |
| 202 | + $lines[] = trim($line, ' *'); |
| 203 | + } |
| 204 | + |
| 205 | + return str_replace('"', '""', implode(' ', $lines)); |
| 206 | + } |
| 207 | + |
| 208 | + private function determineDataType(array $parameter): string |
| 209 | + { |
| 210 | + $dataType = $parameter['dataType'] ?? 'string'; |
| 211 | + $transform = [ |
| 212 | + 'float' => 'number', |
| 213 | + 'datetime' => 'string', |
| 214 | + ]; |
| 215 | + if (\array_key_exists($dataType, $transform)) { |
| 216 | + $dataType = $transform[$dataType]; |
| 217 | + } |
| 218 | + |
| 219 | + return $dataType; |
| 220 | + } |
| 221 | +} |
0 commit comments