Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 3e27387

Browse files
committed
Update conflict constraint for "nelmio/api-doc-bundle" in order to allow version 3
1 parent 5b0346e commit 3e27387

23 files changed

+1253
-160
lines changed

docs/reference/api.rst

+21-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ Here's the configuration we used, you may adapt it to your needs:
3030
enabled: true
3131
validate: true
3232
33+
.. code-block:: yaml
34+
35+
# config/packages/framework.yaml
36+
37+
framework:
38+
error_controller: 'FOS\RestBundle\Controller\ExceptionController::showAction'
39+
40+
.. code-block:: yaml
41+
42+
# config/packages/twig.yaml
43+
44+
twig:
45+
exception_controller: null
46+
3347
.. code-block:: yaml
3448
3549
# config/packages/sensio_framework_extra.yaml
@@ -67,10 +81,14 @@ In order to activate the ReST API, you'll also need to add this to your routing:
6781
prefix: /api/doc
6882
6983
sonata_api_news:
70-
type: rest
71-
prefix: /api
84+
prefix: /api/news
7285
resource: '@SonataNewsBundle/Resources/config/routing/api.xml'
7386
87+
# or for nelmio/api-doc-bundle v3
88+
#sonata_api_news:
89+
# prefix: /api/news
90+
# resource: "@SonataNewsBundle/Resources/config/routing/api_nelmio_v3.xml"
91+
7492
Serialization
7593
-------------
7694

@@ -81,5 +99,5 @@ The taxonomy is as follows:
8199
* ``sonata_api_read`` is the group used to display entities
82100
* ``sonata_api_write`` is the group used for input entities (when used instead of forms)
83101

84-
If you wish to customize the outputted data, feel free to setup your own serialization options
102+
If you wish to customize the outputted data, feel free to set up your own serialization options
85103
by configuring `JMSSerializer <https://jmsyst.com/libs/serializer>`_ with those groups.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
}

src/Controller/Api/CommentController.php

+31-21
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
use FOS\RestBundle\Controller\Annotations as REST;
1717
use FOS\RestBundle\View\View;
18-
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
18+
use Nelmio\ApiDocBundle\Annotation\Model;
19+
use Nelmio\ApiDocBundle\Annotation\Operation;
1920
use Sonata\NewsBundle\Model\Comment;
2021
use Sonata\NewsBundle\Model\CommentManagerInterface;
22+
use Swagger\Annotations as SWG;
2123
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
2224

2325
/**
@@ -41,16 +43,18 @@ public function __construct(CommentManagerInterface $commentManager)
4143
/**
4244
* Retrieves a specific comment.
4345
*
44-
* @ApiDoc(
45-
* resource=true,
46-
* requirements={
47-
* {"name"="id", "dataType"="string", "description"="Comment identifier"}
48-
* },
49-
* output={"class"="Sonata\NewsBundle\Model\Comment", "groups"={"sonata_api_read"}},
50-
* statusCodes={
51-
* 200="Returned when successful",
52-
* 404="Returned when comment is not found"
53-
* }
46+
* @Operation(
47+
* tags={"/api/news/comments/{id}"},
48+
* summary="Retrieves a specific comment.",
49+
* @SWG\Response(
50+
* response="200",
51+
* description="Returned when successful",
52+
* @SWG\Schema(ref=@Model(type="Sonata\NewsBundle\Model\Comment"))
53+
* ),
54+
* @SWG\Response(
55+
* response="404",
56+
* description="Returned when comment is not found"
57+
* )
5458
* )
5559
*
5660
* @REST\View(serializerGroups={"sonata_api_read"}, serializerEnableMaxDepthChecks=true)
@@ -69,15 +73,21 @@ public function getCommentAction($id)
6973
/**
7074
* Deletes a comment.
7175
*
72-
* @ApiDoc(
73-
* requirements={
74-
* {"name"="id", "dataType"="string", "description"="Comment identifier"}
75-
* },
76-
* statusCodes={
77-
* 200="Returned when comment is successfully deleted",
78-
* 400="Returned when an error has occurred while comment deletion",
79-
* 404="Returned when unable to find comment"
80-
* }
76+
* @Operation(
77+
* tags={"/api/news/comments/{id}"},
78+
* summary="Deletes a comment.",
79+
* @SWG\Response(
80+
* response="200",
81+
* description="Returned when comment is successfully deleted"
82+
* ),
83+
* @SWG\Response(
84+
* response="400",
85+
* description="Returned when an error has occurred while comment deletion"
86+
* ),
87+
* @SWG\Response(
88+
* response="404",
89+
* description="Returned when unable to find comment"
90+
* )
8191
* )
8292
*
8393
* @param string $id Comment identifier
@@ -113,7 +123,7 @@ protected function getComment($id)
113123
$comment = $this->commentManager->find($id);
114124

115125
if (null === $comment) {
116-
throw new NotFoundHttpException(sprintf('Comment (%d) not found', $id));
126+
throw new NotFoundHttpException(sprintf('Comment (%s) not found', $id));
117127
}
118128

119129
return $comment;

0 commit comments

Comments
 (0)